From 11c2962e89625984c0e0b216cafeb099a45a0d02 Mon Sep 17 00:00:00 2001 From: Kevin Bond Date: Sat, 20 Apr 2024 11:01:55 -0400 Subject: [PATCH 1/2] doc: update --- docs/index.rst | 550 ++++++++++++++++++------------------------------- 1 file changed, 197 insertions(+), 353 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index 2f26ebf6..8f8ba79b 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -96,17 +96,17 @@ For the remainder of the documentation, the following sample entities will be us // ... getters/setters } -Model Factories ---------------- +Persistent Object Factories +--------------------------- -The nicest way to use Foundry is to generate one *factory* class per entity. You can skip this -and use `Anonymous Factories`_, but *model factories* give you IDE auto-completion -and access to other useful features. +The nicest way to use Foundry is to generate one *factory* class per ORM entity or MongoDB document. +You can skip this and use `Anonymous Factories`_, but *persistent object factories* give you IDE +auto-completion and access to other useful features. Generate ~~~~~~~~ -Create a model factory for one of your entities with the maker command: +Create a persistent object factory for one of your entities with the maker command: .. code-block:: terminal @@ -129,65 +129,67 @@ This command will generate a ``PostFactory`` class that looks like this: use App\Entity\Post; use App\Repository\PostRepository; - use Zenstruck\Foundry\RepositoryProxy; - use Zenstruck\Foundry\ModelFactory; - use Zenstruck\Foundry\Proxy; + use Zenstruck\Foundry\Persistence\PersistentProxyObjectFactory; + use Zenstruck\Foundry\Persistence\Proxy; + use Zenstruck\Foundry\Persistence\ProxyRepositoryDecorator; /** - * @extends ModelFactory + * @extends PersistentProxyObjectFactory * - * @method Post|Proxy create(array|callable $attributes = []) - * @method static Post|Proxy createOne(array $attributes = []) - * @method static Post|Proxy find(object|array|mixed $criteria) - * @method static Post|Proxy findOrCreate(array $attributes) - * @method static Post|Proxy first(string $sortedField = 'id') - * @method static Post|Proxy last(string $sortedField = 'id') - * @method static Post|Proxy random(array $attributes = []) - * @method static Post|Proxy randomOrCreate(array $attributes = [])) - * @method static PostRepository|RepositoryProxy repository() - * @method static Post[]|Proxy[] all() - * @method static Post[]|Proxy[] createMany(int $number, array|callable $attributes = []) - * @method static Post[]&Proxy[] createSequence(iterable|callable $sequence) - * @method static Post[]|Proxy[] findBy(array $attributes) - * @method static Post[]|Proxy[] randomRange(int $min, int $max, array $attributes = [])) - * @method static Post[]|Proxy[] randomSet(int $number, array $attributes = [])) + * @method Post|Proxy create(array|callable $attributes = []) + * @method static Post|Proxy createOne(array $attributes = []) + * @method static Post|Proxy find(object|array|mixed $criteria) + * @method static Post|Proxy findOrCreate(array $attributes) + * @method static Post|Proxy first(string $sortedField = 'id') + * @method static Post|Proxy last(string $sortedField = 'id') + * @method static Post|Proxy random(array $attributes = []) + * @method static Post|Proxy randomOrCreate(array $attributes = []) + * @method static PostRepository|ProxyRepositoryDecorator repository() + * @method static Post[]|Proxy[] all() + * @method static Post[]|Proxy[] createMany(int $number, array|callable $attributes = []) + * @method static Post[]|Proxy[] createSequence(iterable|callable $sequence) + * @method static Post[]|Proxy[] findBy(array $attributes) + * @method static Post[]|Proxy[] randomRange(int $min, int $max, array $attributes = []) + * @method static Post[]|Proxy[] randomSet(int $number, array $attributes = []) */ - final class PostFactory extends ModelFactory + final class PostFactory extends PersistentProxyObjectFactory { /** - * @see https://github.com/zenstruck/foundry#factories-as-services + * @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#factories-as-services * * @todo inject services if required */ public function __construct() { - parent::__construct(); + } + + public static function class(): string + { + return Post::class; } /** - * @see https://github.com/zenstruck/foundry#model-factories + * @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#model-factories * * @todo add your default values here */ - protected function getDefaults(): array + protected function defaults(): array|callable { - return []; + return [ + 'createdAt' => \DateTimeImmutable::createFromMutable(self::faker()->dateTime()), + 'title' => self::faker()->text(255), + ]; } /** - * @see https://github.com/zenstruck/foundry#initialization + * @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#initialization */ - protected function initialize(): self + protected function initialize(): static { return $this - // ->afterInstantiate(function(Post $post) {}) + // ->afterInstantiate(function(Post $post): void {}) ; } - - protected static function getClass(): string - { - return Post::class; - } } .. tip:: @@ -207,50 +209,15 @@ This command will generate a ``PostFactory`` class that looks like this: zenstruck_foundry: make_factory: default_namespace: 'App\\MyFactories' - make_story: - default_namespace: 'App\\MyStories' You can override this configuration by using the ``--namespace`` option. - -.. note:: - - The generated ``@method`` docblocks above enable autocompletion with PhpStorm but - cause errors with PHPStan and Psalm. To support PHPStan or Psalm for your factory's, you need to *also* - add the following dockblocks (replace ``phpstan-`` prefix by ``psalm-`` accordingly to your static analysis tool): - - .. code-block:: php - - /** - * ... - * - * @phpstan-method Proxy create(array|callable $attributes = []) - * @phpstan-method static Proxy createOne(array $attributes = []) - * @phpstan-method static Proxy find(object|array|mixed $criteria) - * @phpstan-method static Proxy findOrCreate(array $attributes) - * @phpstan-method static Proxy first(string $sortedField = 'id') - * @phpstan-method static Proxy last(string $sortedField = 'id') - * @phpstan-method static Proxy random(array $attributes = []) - * @phpstan-method static Proxy randomOrCreate(array $attributes = []) - * @phpstan-method static RepositoryProxy repository() - * @phpstan-method static list> all() - * @phpstan-method static list> createMany(int $number, array|callable $attributes = []) - * @phpstan-method static list> createSequence(iterable|callable $sequence) - * @phpstan-method static list> findBy(array $attributes) - * @phpstan-method static list> randomRange(int $min, int $max, array $attributes = []) - * @phpstan-method static list> randomSet(int $number, array $attributes = []) - */ - final class PostFactory extends ModelFactory - { - // ... - } - -In the ``getDefaults()``, you can return an array of all default values that any new object +In the ``defaults()``, you can return an array of all default values that any new object should have. `Faker`_ is available to easily get random data: .. code-block:: php - protected function getDefaults(): array + protected function defaults(): array { return [ // Symfony's property-access component is used to populate the properties @@ -262,7 +229,7 @@ should have. `Faker`_ is available to easily get random data: .. tip:: - It is best to have ``getDefaults()`` return the attributes to persist a valid object + It is best to have ``defaults()`` return the attributes to persist a valid object (all non-nullable fields). .. tip:: @@ -272,8 +239,8 @@ should have. `Faker`_ is available to easily get random data: .. note:: - ``getDefaults()`` is called everytime a factory is instantiated (even if you don't end up - creating it). `Lazy values`_ allows you to ensure the value is only calculated when/if it's needed. + ``defaults()`` is called everytime a factory is instantiated (even if you don't end up + creating it). `Lazy Values`_ allows you to ensure the value is only calculated when/if it's needed. Using your Factory ~~~~~~~~~~~~~~~~~~ @@ -282,7 +249,7 @@ Using your Factory use App\Factory\PostFactory; - // create/persist Post with random data from `getDefaults()` + // create/persist Post with random data from `defaults()` PostFactory::createOne(); // or provide values for some properties (others will be random) @@ -294,10 +261,10 @@ Using your Factory // the "Proxy" magically calls the underlying Post methods and is type-hinted to "Post" $title = $post->getTitle(); // getTitle() can be autocompleted by your IDE! - // if you need the actual Post object, use ->object() - $realPost = $post->object(); + // if you need the actual Post object, use ->_real() + $realPost = $post->_real(); - // create/persist 5 Posts with random data from getDefaults() + // create/persist 5 Posts with random data from defaults() PostFactory::createMany(5); // returns Post[]|Proxy[] PostFactory::createMany(5, ['title' => 'My Title']); @@ -345,37 +312,32 @@ Using your Factory $posts = PostFactory::randomRange(0, 5); // array containing 0-5 "Post|Proxy" objects $posts = PostFactory::randomRange(0, 5, ['author' => 'kevin']); // filter by the passed attributes -Reusable Model Factory "States" -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Reusable Factory "States" +~~~~~~~~~~~~~~~~~~~~~~~~~ -You can add any methods you want to your model factories (i.e. static methods that create an object in a certain way) but +You can add any methods you want to your factories (i.e. static methods that create an object in a certain way) but you can also add *states*: .. code-block:: php - namespace App\Factory; - - use App\Entity\Post; - use Zenstruck\Foundry\ModelFactory; - - final class PostFactory extends ModelFactory + final class PostFactory extends PersistentProxyObjectFactory { // ... public function published(): self { // call setPublishedAt() and pass a random DateTime - return $this->addState(['published_at' => self::faker()->dateTime()]); + return $this->with(['published_at' => self::faker()->dateTime()]); } public function unpublished(): self { - return $this->addState(['published_at' => null]); + return $this->with(['published_at' => null]); } public function withViewCount(int $count = null): self { - return $this->addState(function () use ($count) { + return $this->with(function () use ($count) { return ['view_count' => $count ?? self::faker()->numberBetween(0, 10000)]; }); } @@ -418,19 +380,19 @@ instantiation. use function Zenstruck\Foundry\faker; // The first argument to "new()" allows you to overwrite the default - // values that are defined in the `PostFactory::getDefaults()` + // values that are defined in the `PostFactory::defaults()` $posts = PostFactory::new(['title' => 'Post A']) - ->withAttributes([ + ->with([ 'body' => 'Post Body...', // CategoryFactory will be used to create a new Category for each Post 'category' => CategoryFactory::new(['name' => 'php']), ]) - ->withAttributes([ + ->with([ // Proxies are automatically converted to their wrapped object 'category' => CategoryFactory::createOne(), ]) - ->withAttributes(function() { return ['createdAt' => faker()->dateTime()]; }) // see faker section below + ->with(function() { return ['createdAt' => faker()->dateTime()]; }) // see faker section below // create "2" Post's ->many(2)->create(['title' => 'Different Title']) @@ -450,9 +412,8 @@ instantiation. .. note:: - Attributes passed to the ``create*`` methods are merged with any attributes set via ``getDefaults()`` - and ``withAttributes()``. - + Attributes passed to the ``create*`` methods are merged with any attributes set via ``defaults()`` + and ``with()``. Sequences ~~~~~~~~~ @@ -498,12 +459,8 @@ random data for your factories: .. code-block:: php - use Zenstruck\Foundry\Factory; use function Zenstruck\Foundry\faker; - Factory::faker()->name(); // random name - - // alternatively, use the helper function faker()->email(); // random email .. note:: @@ -585,11 +542,11 @@ they were added. ->afterPersist(function() {}) ; -You can also add hooks directly in your model factory class: +You can also add hooks directly in your factory class: .. code-block:: php - protected function initialize(): self + protected function initialize(): static { return $this ->afterPersist(function() {}) @@ -601,20 +558,15 @@ Read `Initialization`_ to learn more about the ``initialize()`` method. Initialization ~~~~~~~~~~~~~~ -You can override your model factory's ``initialize()`` method to add default state/logic: +You can override your factory's ``initialize()`` method to add default state/logic: .. code-block:: php - namespace App\Factory; - - use App\Entity\Post; - use Zenstruck\Foundry\ModelFactory; - - final class PostFactory extends ModelFactory + final class PostFactory extends PersistentProxyObjectFactory { // ... - protected function initialize(): self + protected function initialize(): static { return $this ->published() // published by default @@ -642,27 +594,27 @@ You can customize the instantiator in several ways: use App\Entity\Post; use App\Factory\PostFactory; - use Zenstruck\Foundry\Instantiator; + use Zenstruck\Foundry\Object\Instantiator; // set the instantiator for the current factory PostFactory::new() // instantiate the object without calling the constructor - ->instantiateWith((new Instantiator())->withoutConstructor()) + ->instantiateWith(Instantiator::withoutConstructor()) // "foo" and "bar" attributes are ignored when instantiating - ->instantiateWith((new Instantiator())->allowExtraAttributes(['foo', 'bar'])) + ->instantiateWith(Instantiator::withConstructor()->allowExtra(['foo', 'bar'])) // all extra attributes are ignored when instantiating - ->instantiateWith((new Instantiator())->allowExtraAttributes()) + ->instantiateWith(Instantiator::withConstructor()->allowExtra()) // force set "title" and "body" when instantiating - ->instantiateWith((new Instantiator())->alwaysForceProperties(['title', 'body'])) + ->instantiateWith(Instantiator::withConstructor()->alwaysForce(['title', 'body'])) // never use setters, always "force set" properties (even private/protected, does not use setter) - ->instantiateWith((new Instantiator())->alwaysForceProperties()) + ->instantiateWith(Instantiator::withConstructor()->alwaysForce()) // can combine the different "modes" - ->instantiateWith((new Instantiator())->withoutConstructor()->allowExtraAttributes()->alwaysForceProperties()) + ->instantiateWith(Instantiator::withoutConstructor()->allowExtra()->alwaysForce()) // the instantiator is just a callable, you can provide your own ->instantiateWith(function(array $attributes, string $class): object { @@ -679,7 +631,7 @@ instantiators): when@dev: # see Bundle Configuration section about sharing this in the test environment zenstruck_foundry: instantiator: - without_constructor: true # always instantiate objects without calling the constructor + use_constructor: false # always instantiate objects without calling the constructor allow_extra_attributes: true # always ignore extra attributes always_force_properties: true # always "force set" properties # or @@ -695,7 +647,7 @@ Factory's are immutable: use App\Factory\PostFactory; $factory = PostFactory::new(); - $factory1 = $factory->withAttributes([]); // returns a new PostFactory object + $factory1 = $factory->with([]); // returns a new PostFactory object $factory2 = $factory->instantiateWith(function () {}); // returns a new PostFactory object $factory3 = $factory->beforeInstantiate(function () {}); // returns a new PostFactory object $factory4 = $factory->afterInstantiate(function () {}); // returns a new PostFactory object @@ -752,17 +704,17 @@ The following assumes the ``Comment`` entity has a many-to-one relationship with .. tip:: - It is recommended that the only relationship you define in ``ModelFactory::getDefaults()`` is non-null + It is recommended that the only relationship you define in ``defaults()`` is non-null Many-to-One's. .. tip:: - It is also recommended that your ``ModelFactory::getDefaults()`` return a ``Factory`` and not the created entity. - However, you can use `Lazy values`_ if you need to create the entity in the ``getDefaults()`` method. + It is also recommended that your ``defaults()`` return a ``Factory`` and not the created entity. + However, you can use `Lazy Values`_ if you need to create the entity in the ``defaults()`` method. .. code-block:: php - protected function getDefaults(): array + protected function defaults(): array { return [ // RECOMMENDED @@ -840,10 +792,10 @@ The following assumes the ``Post`` entity has a many-to-many relationship with ` // Example 5: create 3 Posts each with between 0 and 3 unique Tags PostFactory::createMany(3, ['tags' => TagFactory::new()->many(0, 3)]); -Lazy values +Lazy Values ~~~~~~~~~~~ -The ``getDefaults()`` method is called everytime a factory is instantiated (even if you don't end up +The ``defaults()`` method is called everytime a factory is instantiated (even if you don't end up creating it). Sometimes, you might not want your value calculated every time. For example, if you have a value for one of your attributes that: @@ -857,11 +809,11 @@ the LazyValue can be `memoized `_ so use Zenstruck\Foundry\Attributes\LazyValue; - class TaskFactory extends ModelFactory + class TaskFactory extends PersistentProxyObjectFactory { // ... - protected function getDefaults(): array + protected function defaults(): array { $owner = LazyValue::memoize(fn() => UserFactory::createOne()); @@ -873,11 +825,6 @@ the LazyValue can be `memoized `_ so 'owner' => $owner, ]; } - - protected static function getClass(): string - { - return Task::class; - } } .. tip:: @@ -895,13 +842,9 @@ common use-case: encoding a password with the ``UserPasswordHasherInterface`` se // src/Factory/UserFactory.php - namespace App\Factory; - - use App\Entity\User; use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface; - use Zenstruck\Foundry\ModelFactory; - final class UserFactory extends ModelFactory + final class UserFactory extends PersistentProxyObjectFactory { private $passwordHasher; @@ -912,7 +855,12 @@ common use-case: encoding a password with the ``UserPasswordHasherInterface`` se $this->passwordHasher = $passwordHasher; } - protected function getDefaults(): array + public static function class(): string + { + return User::class; + } + + protected function defaults(): array { return [ 'email' => self::faker()->unique()->safeEmail(), @@ -920,7 +868,7 @@ common use-case: encoding a password with the ``UserPasswordHasherInterface`` se ]; } - protected function initialize(): self + protected function initialize(): static { return $this ->afterInstantiate(function(User $user) { @@ -928,11 +876,6 @@ common use-case: encoding a password with the ``UserPasswordHasherInterface`` se }) ; } - - protected static function getClass(): string - { - return User::class; - } } If using a standard Symfony Flex app, this will be autowired/autoconfigured. If not, register the service and tag @@ -958,19 +901,18 @@ Use the factory as normal: Anonymous Factories ~~~~~~~~~~~~~~~~~~~ -Foundry can be used to create factories for entities that you don't have model factories for: +Foundry can be used to create factories for entities that you don't have factories for: .. code-block:: php use App\Entity\Post; - use function Zenstruck\Foundry\anonymous; - use function Zenstruck\Foundry\create; - use function Zenstruck\Foundry\create_many; - use function Zenstruck\Foundry\repository; + use function Zenstruck\Foundry\Persistence\proxy_factory; + use function Zenstruck\Foundry\Persistence\persist_proxy; + use function Zenstruck\Foundry\Persistence\repository; - $factory = anonymous(Post::class); + $factory = proxy_factory(Post::class); - // has the same API as ModelFactory's + // has the same API as non-anonymous factories $factory->create(['field' => 'value']); $factory->many(5)->create(['field' => 'value']); $factory->instantiateWith(function () {}); @@ -1008,25 +950,24 @@ Foundry can be used to create factories for entities that you don't have model f $repository->randomRange(0, 5, ['author' => 'kevin']); // filter by the passed attributes // convenience functions - $entity = create(Post::class, ['field' => 'value']); - $entities = create_many(Post::class, 5, ['field' => 'value']); + $entity = persist_proxy(Post::class, ['field' => 'value']); .. note:: - If your anonymous factory code is getting too complex, this could be a sign you need an explicit model factory class. + If your anonymous factory code is getting too complex, this could be a sign you need an explicit factory class. Delay Flush ~~~~~~~~~~~ -When creating/persisting many factories at once, it can be improve performance +When creating/persisting many factories at once, it can improve performance to instantiate them all without saving to the database, then flush them all at -once. To do this, wrap the operations in a ``Factory::delayFlush()`` callback: +once. To do this, wrap the operations in a ``flush_after()`` callback: .. code-block:: php - use Zenstruck\Foundry\Factory; + use function Zenstruck\Foundry\Persistence\flush_after; - Factory::delayFlush(function() { + flush_after(function() { CategoryFactory::createMany(100); // instantiated/persisted but not flushed TagFactory::createMany(200); // instantiated/persisted but not flushed }); // single flush @@ -1044,9 +985,8 @@ in a ``Proxy`` to optionally save later. use App\Factory\PostFactory; use App\Entity\Post; - use Zenstruck\Foundry\anonymous; - use function Zenstruck\Foundry\instantiate; - use function Zenstruck\Foundry\instantiate_many; + use function Zenstruck\Foundry\Persistence\proxy_factory; + use function Zenstruck\Foundry\object; $post = PostFactory::new()->withoutPersisting()->create(); // returns Post|Proxy $post->setTitle('something else'); // do something with object @@ -1057,7 +997,7 @@ in a ``Proxy`` to optionally save later. $posts = PostFactory::new()->withoutPersisting()->many(5)->create(); // returns Post[]|Proxy[] // anonymous factories: - $factory = anonymous(Post::class); + $factory = proxy_factory(Post::class); $entity = $factory->withoutPersisting()->create(['field' => 'value']); // returns Post|Proxy @@ -1066,29 +1006,28 @@ in a ``Proxy`` to optionally save later. $entities = $factory->withoutPersisting()->many(5)->create(['field' => 'value']); // returns Post[]|Proxy[] // convenience functions - $entity = instantiate(Post::class, ['field' => 'value']); - $entities = instantiate_many(Post::class, 5, ['field' => 'value']); + $entity = object(Post::class, ['field' => 'value']); -If you'd like your model factory to not persist by default, override its ``initialize()`` method to add this behaviour: +If you'd like your factory to not persist by default, override its ``initialize()`` method to add this behaviour: .. code-block:: php - protected function initialize(): self + protected function initialize(): static { return $this ->withoutPersisting() ; } -Now, after creating objects using this factory, you'd have to call ``->save()`` to actually persist them to the database. +Now, after creating objects using this factory, you'd have to call ``->_save()`` to actually persist them to the database. .. tip:: - If you'd like to disable persisting by default for all your model factories: + If you'd like to disable persisting by default for all your object factories: - 1. Create an abstract model factory that extends ``Zenstruck\Foundry\ModelFactory``. + 1. Create an abstract factory that extends ``PersistentProxyObjectFactory``. 2. Override the ``initialize()`` method as shown above. - 3. Have all your model factories extend from this. + 3. Have all your factories extend from this. Using with DoctrineFixturesBundle --------------------------------- @@ -1167,7 +1106,6 @@ Let's look at an example: $this->assertCount(0, $post->getComments()); // 2. "Act" - static::ensureKernelShutdown(); // creating factories boots the kernel; shutdown before creating the client $client = static::createClient(); $client->request('GET', '/posts/post-a'); // Note the slug from the arrange step $client->submitForm('Add', [ @@ -1178,7 +1116,7 @@ Let's look at an example: // 3. "Assert" self::assertResponseRedirects('/posts/post-a'); - $this->assertCount(1, $post->refresh()->getComments()); // Refresh $post from the database and call ->getComments() + $this->assertCount(1, $post->_refresh()->getComments()); // Refresh $post from the database and call ->getComments() CommentFactory::assert()->exists([ // Doctrine repository assertions 'name' => 'John', @@ -1270,7 +1208,7 @@ bundle's configuration: - orm_object_manager_1 - orm_object_manager_2 reset_mode: schema - odm: + mongo: object_managers: - odm_object_manager_1 - odm_object_manager_2 @@ -1288,22 +1226,22 @@ to have `Active Record `_ * $post = PostFactory::createOne(['title' => 'My Title']); // instance of Zenstruck\Foundry\Proxy // get the wrapped object - $realPost = $post->object(); // instance of Post + $realPost = $post->_real(); // instance of Post // call any Post method $post->getTitle(); // "My Title" // set property and save to the database $post->setTitle('New Title'); - $post->save(); + $post->_save(); // refresh from the database - $post->refresh(); + $post->_refresh(); // delete from the database - $post->remove(); + $post->_delete(); - $post->repository(); // repository proxy wrapping PostRepository (see Repository Proxy section below) + $post->_repository(); // repository proxy wrapping PostRepository (see Repository Proxy section below) Force Setting ............. @@ -1313,15 +1251,15 @@ Object proxies have helper methods to access non-public properties of the object .. code-block:: php // set private/protected properties - $post->forceSet('createdAt', new \DateTime()); + $post->_set('createdAt', new \DateTime()); // get private/protected properties - $post->forceGet('createdAt'); + $post->_get('createdAt'); Auto-Refresh ............ -Object proxies have the option to enable *auto refreshing* that removes the need to call ``->refresh()`` before calling +Object proxies have the option to enable *auto refreshing* that removes the need to call ``->_refresh()`` before calling methods on the underlying object. When auto-refresh is enabled, most calls to proxy objects first refresh the wrapped object from the database. @@ -1331,12 +1269,12 @@ object from the database. $post = PostFactory::new(['title' => 'Original Title']) ->create() - ->enableAutoRefresh() + ->_enableAutoRefresh() ; // ... logic that changes the $post title to "New Title" (like your functional test) - $post->getTitle(); // "New Title" (equivalent to $post->refresh()->getTitle()) + $post->getTitle(); // "New Title" (equivalent to $post->_refresh()->getTitle()) Without auto-refreshing enabled, the above call to ``$post->getTitle()`` would return "Original Title". @@ -1352,7 +1290,7 @@ Without auto-refreshing enabled, the above call to ``$post->getTitle()`` would r $post = PostFactory::new(['title' => 'Original Title', 'body' => 'Original Body']) ->create() - ->enableAutoRefresh() + ->_enableAutoRefresh() ; $post->setTitle('New Title'); @@ -1367,32 +1305,25 @@ Without auto-refreshing enabled, the above call to ``$post->getTitle()`` would r $post = PostFactory::new(['title' => 'Original Title', 'body' => 'Original Body']) ->create() - ->enableAutoRefresh() + ->_enableAutoRefresh() ; - $post->disableAutoRefresh(); + $post->_disableAutoRefresh(); $post->setTitle('New Title'); // or using ->forceSet('title', 'New Title') $post->setBody('New Body'); // or using ->forceSet('body', 'New Body') - $post->enableAutoRefresh(); + $post->_enableAutoRefresh(); $post->save(); $post->getBody(); // "New Body" $post->getTitle(); // "New Title" - // alternatively, use the ->withoutAutoRefresh() helper which first disables auto-refreshing, then re-enables after + // alternatively, use the ->_withoutAutoRefresh() helper which first disables auto-refreshing, then re-enables after // executing the callback. - $post->withoutAutoRefresh(function (Post $post) { // can pass either Post or Proxy to the callback + $post->_withoutAutoRefresh(function (Post $post) { // can pass either Post or Proxy to the callback $post->setTitle('New Title'); $post->setBody('New Body'); }); - $post->save(); - - // if force-setting properties, you can use the ->forceSetAll() helper: - $post->forceSetAll([ - 'title' => 'New Title', - 'body' => 'New Body', - ]); - $post->save(); + $post->_save(); .. note:: @@ -1417,12 +1348,12 @@ This library provides a *Repository Proxy* that wraps your object repositories t use App\Entity\Post; use App\Factory\PostFactory; - use function Zenstruck\Foundry\repository; + use function Zenstruck\Foundry\Persistence\repository; // instance of RepositoryProxy that wraps PostRepository $repository = PostFactory::repository(); - // alternative to above for proxying repository you haven't created model factories for + // alternative to above for proxying repository you haven't created factories for $repository = repository(Post::class); // helpful methods - all returned object(s) are proxied @@ -1455,17 +1386,12 @@ This library provides a *Repository Proxy* that wraps your object repositories t Assertions ~~~~~~~~~~ -Both object proxies and your ModelFactory have helpful PHPUnit assertions: +Your object factory's have helpful PHPUnit assertions: .. code-block:: php use App\Factory\PostFactory; - $post = PostFactory::createOne(); - - $post->assertPersisted(); - $post->assertNotPersisted(); - PostFactory::assert()->empty(); PostFactory::assert()->count(3); PostFactory::assert()->countGreaterThan(3); @@ -1539,7 +1465,7 @@ It is possible to use factories in .. note:: - Be sure your data provider returns only instances of ``ModelFactory`` and you do not try to call ``->create()`` on them. + Be sure your data provider returns only instances of ``Factory`` and you do not try to call ``->create()`` on them. Data providers are computed early in the phpunit process before Foundry is booted. .. note:: @@ -1549,7 +1475,7 @@ It is possible to use factories in .. note:: - Still for the same reason, if `Faker`_ is needed along with ``->withAttributes()`` within a data provider, you'll need + Still for the same reason, if `Faker`_ is needed along with ``->with()`` within a data provider, you'll need to pass attributes as a *callable*. Given the data provider of the previous example, here is ``PostFactory::published()`` @@ -1559,10 +1485,10 @@ It is possible to use factories in public function published(): self { // This won't work in a data provider! - // return $this->withAttributes(['published_at' => self::faker()->dateTime()]); + // return $this->with(['published_at' => self::faker()->dateTime()]); // use this instead: - return $this->withAttributes( + return $this->with( static fn() => [ 'published_at' => self::faker()->dateTime() ] @@ -1571,7 +1497,7 @@ It is possible to use factories in .. tip:: - ``ModelFactory::new()->many()`` and ``ModelFactory::new()->sequence()`` return a special ``FactoryCollection`` object + ``ObjectFactory::new()->many()`` and ``ObjectFactory::new()->sequence()`` return a special ``FactoryCollection`` object which can be used to generate data providers: .. code-block:: php @@ -1739,15 +1665,15 @@ Pre-Encode Passwords .................... Pre-encode user passwords with a known value via ``bin/console security:encode-password`` and set this in -``ModelFactory::getDefaults()``. Add the known value as a ``const`` on your factory: +``defaults()``. Add the known value as a ``const`` on your factory: .. code-block:: php - class UserFactory extends ModelFactory + class UserFactory extends PersistentProxyObjectFactory { public const DEFAULT_PASSWORD = '1234'; // the password used to create the pre-encoded version below - protected function getDefaults(): array + protected function defaults(): array { return [ // ... @@ -1793,11 +1719,10 @@ You will need to configure manually Foundry. Unfortunately, this may mean duplic // tests/bootstrap.php // ... - Zenstruck\Foundry\Test\TestState::configure( - instantiator: (new Zenstruck\Foundry\Instantiator()) - ->withoutConstructor() - ->allowExtraAttributes() - ->alwaysForceProperties(), + Zenstruck\Foundry\Test\UnitTestConfig::configure( + instantiator: Zenstruck\Foundry\Object\Instantiator::withoutConstructor() + ->allowExtra() + ->alwaysForce(), faker: Faker\Factory::create('fr_FR') ); @@ -1912,10 +1837,6 @@ If your stories require dependencies, you can define them as a service: If using a standard Symfony Flex app, this will be autowired/autoconfigured. If not, register the service and tag with ``foundry.story``. -.. note:: - - The provided bundle is required for stories as services. - Story State ~~~~~~~~~~~ @@ -2046,150 +1967,73 @@ This way, there is just one place to set your config. Full Default Bundle Configuration ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. configuration-block:: - - .. code-block:: yaml - - zenstruck_foundry: - - # Whether to auto-refresh proxies by default (https://github.com/zenstruck/foundry#auto-refresh) - auto_refresh_proxies: true - - # Configure faker to be used by your factories. - faker: - - # Change the default faker locale. - locale: null # Example: fr_FR - - # Random number generator seed to produce the same fake values every run - seed: null # Example: 1234 - - # Customize the faker service. - service: null # Example: my_faker - - # Configure the default instantiator used by your factories. - instantiator: - - # Whether or not to call an object's constructor during instantiation. - without_constructor: false - - # Whether or not to allow extra attributes. - allow_extra_attributes: false - - # Whether or not to skip setters and force set object properties (public/private/protected) directly. - always_force_properties: false - - # Customize the instantiator service. - service: null # Example: my_instantiator - - # Configure the database reset mechanism - database_resetter: - - # Config related to ORM - orm: - - # Connections to reset. If empty, the default connection is used. - connections: [] - - # Object managers to reset. If empty, the default manager is used. - object_managers: [] - - # Whether to use doctrine:schema:update or migrations when resetting schema. - reset_mode: schema # "schema" or "migrate" - - # Config related to ODM - odm: - - # Object managers to reset. If empty, the default manager is used. - object_managers: [] - - # Add global state. - global_state: [] - - # Configure Foundry's make:factory command - make_factory: - - # Namespace to use for make:factory. Is overridden by --namespace option - default_namespace: 'Factory' - - # Configure Foundry's make:story command - make_story: - # Namespace to use for make:story. Is overridden by --namespace option - default_namespace: 'App\\MyStories' - .. code-block:: php - - $config->extension('zenstruck_foundry', [ - - // Whether to auto-refresh proxies by default (https://github.com/zenstruck/foundry#auto-refresh) - 'auto_refresh_proxies' => false, +.. code-block:: yaml - // Configure faker to be used by your factories. - 'faker' => [ + zenstruck_foundry: - // Change the default faker locale. - 'locale' => null, + # Whether to auto-refresh proxies by default (https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#auto-refresh) + auto_refresh_proxies: null - // Random number generator seed to produce the same fake values every run - 'seed' => null, + # Configure faker to be used by your factories. + faker: - // Customize the faker service. - 'service' => null - ], + # Change the default faker locale. + locale: null # Example: fr_FR - // Configure the default instantiator used by your factories. - 'instantiator' => [ + # Random number generator seed to produce the same fake values every run + seed: null # Example: '1234' - // Whether or not to call an object's constructor during instantiation. - 'without_constructor' => false, + # Customize the faker service. + service: null # Example: my_faker - // Whether or not to allow extra attributes. - 'allow_extra_attributes' => false, + # Configure the default instantiator used by your factories. + instantiator: - // Whether or not to skip setters and force set object properties (public/private/protected) directly. - 'always_force_properties' => false, + # Use the constructor to instantiate objects. + use_constructor: ~ - // Customize the instantiator service. - 'service' => null - ] + # Whether or not to allow extra attributes. + allow_extra_attributes: false - // Configure the database reset mechanism - 'database_resetter' => [ + # Whether or not to skip setters and force set object properties (public/private/protected) directly. + always_force_properties: false - // Config related to ORM - 'orm' => [ + # Customize the instantiator service. + service: null # Example: my_instantiator + orm: + reset: - // Connections to reset. If empty, the default connection is used. - 'connections' => [], + # DBAL connections to reset with ResetDatabase trait + connections: - // Whether or not to allow extra attributes. - 'object_managers' => false, + # Default: + - default - // Whether to use doctrine:schema:update or migrations when resetting schema. - 'reset_mode' => 'schema', // 'schema' or 'migration' - ], + # Entity Managers to reset with ResetDatabase trait + entity_managers: - // Config related to ODM - 'odm' => [ + # Default: + - default - // Whether or not to allow extra attributes. - 'object_managers' => false, - ], + # Reset mode to use with ResetDatabase trait + mode: schema # One of "schema"; "migrate" + mongo: + reset: - ], + # Document Managers to reset with ResetDatabase trait + document_managers: - // Add global state - 'global_state' => [], + # Default: + - default - // Configure Foundry's make:factory command - 'make_factory' => [ + # Array of stories that should be used as global state. + global_state: [] - // Namespace to use for make:factory. Is overridden by --namespace option - 'default_namespace' => 'Factory' - ], + make_factory: - 'make_story' => [ + # Default namespace where factories will be created by maker. + default_namespace: Factory + make_story: - // Namespace to use for make:story. Is overridden by --namespace option - 'default_namespace' => 'Story' - ] - ]); + # Default namespace where stories will be created by maker. + default_namespace: Story From 575ac8b29340eb8d4fbc21225ddebe54cc5695ff Mon Sep 17 00:00:00 2001 From: Nicolas PHILIPPE Date: Mon, 3 Jun 2024 11:04:54 +0200 Subject: [PATCH 2/2] docs: update last pieces for 1.x --- docs/index.rst | 66 +++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 62 insertions(+), 4 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index 8f8ba79b..57dbe1f8 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -196,6 +196,11 @@ This command will generate a ``PostFactory`` class that looks like this: Using ``make:factory --test`` will generate the factory in ``tests/Factory``. +.. tip:: + + You can also inherit from `Zenstruck\Foundry\Persistence\PersistentObjectFactory`. Which will create regular objects + without proxy (see :ref:`Proxy object section ` for more information). + .. tip:: You can globally configure which namespace the factories will be generated in: @@ -212,6 +217,38 @@ This command will generate a ``PostFactory`` class that looks like this: You can override this configuration by using the ``--namespace`` option. +.. note:: + + The generated ``@method`` docblocks above enable autocompletion with PhpStorm but + causes errors with PHPStan. To support PHPStan for your factory's, you need to *also* + add the following dockblocks: + + .. code-block:: php + + /** + * ... + * + * @phpstan-method Proxy create(array|callable $attributes = []) + * @phpstan-method static Proxy createOne(array $attributes = []) + * @phpstan-method static Proxy find(object|array|mixed $criteria) + * @phpstan-method static Proxy findOrCreate(array $attributes) + * @phpstan-method static Proxy first(string $sortedField = 'id') + * @phpstan-method static Proxy last(string $sortedField = 'id') + * @phpstan-method static Proxy random(array $attributes = []) + * @phpstan-method static Proxy randomOrCreate(array $attributes = []) + * @phpstan-method static list> all() + * @phpstan-method static list> createMany(int $number, array|callable $attributes = []) + * @phpstan-method static list> createSequence(array|callable $sequence) + * @phpstan-method static list> findBy(array $attributes) + * @phpstan-method static list> randomRange(int $min, int $max, array $attributes = []) + * @phpstan-method static list> randomSet(int $number, array $attributes = []) + * @phpstan-method static RepositoryProxy repository() + */ + final class PostFactory extends ModelFactory + { + // ... + } + In the ``defaults()``, you can return an array of all default values that any new object should have. `Faker`_ is available to easily get random data: @@ -974,12 +1011,19 @@ once. To do this, wrap the operations in a ``flush_after()`` callback: .. _without-persisting: +Not-persisted objects factory +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When dealing with objects which are not aimed to be persisted, you can make your factory inherit from +`Zenstruck\Foundry\ObjectFactory`. This will create plain objects, that does not interact with database (these objects +won't be wrapped with a :ref:`proxy object `). + Without Persisting ~~~~~~~~~~~~~~~~~~ -Factories can also create objects without persisting them. This can be useful for unit tests where you just want to test -the behaviour of the actual object or for creating objects that are not entities. When created, they are still wrapped -in a ``Proxy`` to optionally save later. +"Persitent factories" can also create objects without persisting them. This can be useful for unit tests where you just +want to test the behaviour of the actual object or for creating objects that are not entities. When created, they are +still wrapped in a ``Proxy`` to optionally save later. .. code-block:: php @@ -1213,6 +1257,8 @@ bundle's configuration: - odm_object_manager_1 - odm_object_manager_2 +.. _object-proxy: + Object Proxy ~~~~~~~~~~~~ @@ -1261,7 +1307,8 @@ Auto-Refresh Object proxies have the option to enable *auto refreshing* that removes the need to call ``->_refresh()`` before calling methods on the underlying object. When auto-refresh is enabled, most calls to proxy objects first refresh the wrapped -object from the database. +object from the database. This is mainly useful with "integration" test which interacts with your database and Symfony's +kernel. .. code-block:: php @@ -1339,6 +1386,17 @@ Without auto-refreshing enabled, the above call to ``$post->getTitle()`` would r zenstruck_foundry: auto_refresh_proxies: true/false +Factory without proxy +..................... + +It is possible to create factories which do not create "proxified" objects. Instead of making your factory inherit from +`PersistentProxyObjectFactory`, you can inherit from `PersistentObjectFactory`. Your factory will then directly return +the "real" object, which won't be wrapped by `Proxy` class. + +.. warning:: + + Be aware that your object won't refresh automatically if they are not wrapped with a proxy. + Repository Proxy ~~~~~~~~~~~~~~~~