vierge-noire / cakephp-fixture-factories

CakePHP Fixture Factories
https://vierge-noire.github.io/
MIT License
85 stars 20 forks source link

Is the parent entity data available in a `withWhatever()` method? #133

Closed dreamingmind closed 3 years ago

dreamingmind commented 3 years ago

Thank you for the amazing tool!

In a situation like this:

ParentFactory::make(3)
   ->withSomeAssociation()
   ->persist();

Is there a way to get access to the current Parent entity data while putting together the 'SomeAssociation' record?

I guess what I'm looking for is something like this:

ParentFactory::make(3)
   ->withSomeAssociation($current_parent)
   ->persist();

Although that code wouldn't actually work. But it gives an idea of what I'm trying to accomplish.

dreamingmind commented 3 years ago

I think I found my answer. Perhaps someone will know of a better way.

In the ParentFactory class I would define withSomeAssociation() along these lines:

public function withSomeAssociation($times = 1): ParentFactory
{
    return $this->with(
        'SomeAssociation',
        SomeAssociationFactory::makeFrom($this->getDataCompiler()->getCompiledTemplateData(), $times)
    );
}

Then in SomeAssociationFactory I could do something along these lines:

public static function makeFrom($parentData = [], int $times = 1): SomeAssociationFactory
{
   $makeParameter = self::processParent($parentData);
    return self::make($makeParameter, $times);
}
pabloelcolombiano commented 3 years ago

The getDataCompiler is an internal method, you should not need to use it. I should mark the DataCompiler and other internal classes as in internal in the future.

If you are saving a country and a city, you can do

$country = CountryFactory::make()->with('Cities')->persist();
$city = $country->cities[0];

If you already had an entity country created somewhere in your test, and you need to create a city associated with that particular country, you may do

$city = CityFactory::make()->with('Country', $country)->persist();

I have added annotations in the coming CakePHP 4.3 compliant version. See here. But you can already do that your self, in order to help your IDE perform autocompletion on the entities generated with the factories.

I suspect the solution you are proposing is not necessary, you are probably simply not taking the problem from the right end. Feel free to propose another example based on the Countries/Cities simplistic schema, if you are still blocked. And also a large source of documentation are the tests themselves. You might find some inspiring tricks there, though you'll have to make your way through that small jungle...

dreamingmind commented 3 years ago

I can get the entities I want to create with no problem using normal syntax. I am just trying to make the resulting fixture data a little more human readable, a personal, stylistic goal.

Imagine both Country and City carry a preferred locale value to set a language. The country might be set to some 'most likely' value. A City record could overwrite this value, but when first created it would be set to the Countries value.

Normal FixtureFactory syntax will populate these fields independent of each other. Although the resulting data would be correct, it would not be easy to read an understand.

A stepwise process as you describe in your first example code would work but I was looking for something a little more streamline. I think I found that unless you make the DataCompiler internal :-)

pabloelcolombiano commented 3 years ago

I think I see your scenario now.

If I get you well, you would want both country and city to have the field locale identical. With the current implementation, I would do:

$locale = "Foo";
CityFactory::make(compact('locale'))->with('Country', compact('locale'));

In any case, thanks for sharing your solution. It will be difficult to integrate something clear and general enough, as we do not want to overload the factories with functionalities for user experience reasons.

Putting the DataCompiler internal is a notation only, and should not prevent the development of your solution(s). The getDataCompiler method will remain protected, and not private.

dreamingmind commented 3 years ago

Yes, that's what I was shooting for.

I like that example. I was just trying to formulate something like that based on the code I saw in your ArticlesFactory. It would work well for a single entity pair. Not for n pairs.

Ok! I'm going to dig in with these new ideas and see what I can make work. Thanks for the help.

PS

I've poked around in the tests a a couple of times. They are very instructive! Always good for an ah-ha moment.

dreamingmind commented 3 years ago

This is the solution I ended up with.

My original idea was getting more bizarre and obscure so I ditched it in favor of @pabloelcolombiano 's ideas.

I used a Scenario and a little ...$args processing to make it as flexible as I wanted regarding the number of records generated.


<?php

namespace CakephpFixtureFactories\Test\Scenario;

use CakephpFixtureFactories\Scenario\FixtureScenarioInterface;
use CakephpFixtureFactories\Test\Factory\CityFactory;
use CakephpFixtureFactories\Test\Factory\CountryFactory;
use TestApp\Model\Entity\Country;

class MatchingLocaleScenario implements FixtureScenarioInterface
{

    /**
     * Make City->locale match the associated Country->locale
     * 
     * Pass 0, 1, or 2 arguments
     *
     * If one of the arguments is a Country or Country[]
     * each Country will get one or more City
     * with `locale` values that match the Country's locale
     *
     * If there is no Country provide a new one will be made
     *
     * If one of the arguments is numeric, the number
     * will be used as a count of Cities created for each Country
     *
     * If there is no numeric argument one City will be made for each Country
     *
     */
    public function load(...$args)
    {
        $cityCount = $this->cityCount($args);

        foreach ($this->countries($args) as $country) {
            while ($cityCount > 0) {
                CityFactory::make(['locale' => $country->locale])->persist();
                $cityCount--;
            }
        }
    }

    private function cityCount(array $args)
    {
        if (is_numeric(($args[0] ?? false))) {
            $result = $args[0];
        }
        elseif (is_numeric(($args[1] ?? false))) {
            $result = $args[1];
        }
        return $result ?? 1;
    }

    private function countries(array $args)
    {
        if (is_array(($args[0] ?? false))) {
            $result = $args[0];
        }
        elseif (is_array(($args[1] ?? false))) {
            $result = $args[1];
        }
        elseif (($args[0] ?? false) instanceof Country) {
            $result = [$args[0]];
        }
        if (($args[1] ?? false) instanceof Country) {
            $result = [$args[1]];
        }

        return $result ?? [CountryFactory::make()->persist()];

    }
}```
pabloelcolombiano commented 3 years ago

Nice one!