diff --git a/.gitattributes b/.gitattributes index 1842989ac..4176e26c5 100644 --- a/.gitattributes +++ b/.gitattributes @@ -16,3 +16,6 @@ /phpunit-dama-doctrine.xml.dist export-ignore /phpunit.xml.dist export-ignore /tests export-ignore +/utils/rector/tests export-ignore +/utils/rector/composer.json export-ignore +/utils/rector/phpunit.xml.dist export-ignore diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8be704278..650819641 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -92,6 +92,14 @@ jobs: env: SYMFONY_REQUIRE: ${{ matrix.symfony }} +# - name: Install php cs fixer (needed for maker bundle) +# run: composer bin php-cs-fixer install +# +# # awful hack: no maker test will pass unless we have maker-bundle ^1.55 because of its shipped version of cs-fixer +# # but maker bundle ^1.55 is not compatible with php 8.0 / sf .4 +# - name: Upgrade maker bundle +# run: composer update symfony/maker-bundle + - name: Set up MySQL if: contains(matrix.database, 'mysql') run: sudo /etc/init.d/mysql start @@ -168,7 +176,7 @@ jobs: - name: Setup PHP uses: shivammathur/setup-php@v2 with: - php-version: 8.0 + php-version: 8.1 coverage: none - name: Install dependencies @@ -203,11 +211,41 @@ jobs: - name: Run static analysis run: bin/tools/phpstan/vendor/phpstan/phpstan/phpstan analyse - - name: Install Psalm - run: composer bin psalm install +# there are some problem with psalm and bc layer, around proxy +# - name: Install Psalm +# run: composer bin psalm install +# +# - name: Run Psalm on factories generated with maker +# run: bin/tools/psalm/vendor/vimeo/psalm/psalm - - name: Run Psalm on factories generated with maker - run: bin/tools/psalm/vendor/vimeo/psalm/psalm + test-rector-rules: + name: Test rector rules + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: 8.3 + coverage: none + + - name: Install dependencies + uses: ramsey/composer-install@v2 + with: + composer-options: --prefer-dist + working-directory: "./utils/rector" + + - name: Test + run: vendor/bin/phpunit + shell: bash + working-directory: "./utils/rector" + + - name: Static analysis + run: vendor/bin/phpstan + shell: bash + working-directory: "./utils/rector" fixcs: name: Run php-cs-fixer diff --git a/.gitignore b/.gitignore index f73429c20..3733c26bc 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ /phpunit-dama-doctrine.xml /vendor/ /bin/tools/*/vendor/ +/bin/tools/php-cs-fixer/composer.lock /build/ /.php-cs-fixer.cache /.phpunit.result.cache @@ -13,3 +14,7 @@ /.env.local /docker-compose.override.yaml /tests/Fixtures/Migrations/ + +/utils/rector/vendor +/utils/rector/.phpunit.result.cache +/utils/rector/composer.lock diff --git a/UPGRADE-2.0.md b/UPGRADE-2.0.md new file mode 100644 index 000000000..13de8d7cd --- /dev/null +++ b/UPGRADE-2.0.md @@ -0,0 +1,301 @@ +# Migration guide from Foundry 1.x to 2.0 + +Foundry 2 has changed some of its API. +The global philosophy is still the same. +The main change is that we've introduced a separation between "object" factories, +"persistence" factories and "persistence with proxy" factories. + +When Foundry 1.x was "persistence first", Foundry 2 is "object first". +This would allow more decoupling from the persistence layer. + +## How to + +Every modification needed for a 1.x to 2.0 migration is covered by a deprecation. +You'll to upgrade to the latest 1.x version, and to activate the deprecation helper, make the tests run, and +fix all the deprecations reported. + +Here is an example of how the deprecation helper can be activated. +You should set the `SYMFONY_DEPRECATIONS_HELPER` variable in `phpunit.xml` or `.env.local` file: +```shell +SYMFONY_DEPRECATIONS_HELPER="max[self]=0&max[direct]=0&quiet[]=indirect&quiet[]=other" +``` + +> [!IMPORTANT] +> Some deprecations can also be sent during compilation step. +> These deprecations can be displayed by using the command: `$ bin/console debug:container --deprecations` + +## Rector rules + +In the latest 1.x version, you'll find [rector rules](https://getrector.org/) which will help with the migration path. + +First, you'll need `rector/rector` and `phpstan/phpstan-doctrine`: +```shell +composer require --dev rector/rector phpstan/phpstan-doctrine +``` + +Then, create a `rector.php` file: + +```php +withPaths(['tests']) // add all paths where Foundry is used + ->withSets([FoundrySetList::UP_TO_FOUNDRY_2]) +; +``` + +And finally, run Rector: +```shell +# you can run Rector in "dry run" mode, in order to see which files will be modified +vendor/bin/rector process --dry-run + +# actually modify files +vendor/bin/rector process +``` + +> [!IMPORTANT] +> Rector rules may not totally cover all deprecations (some complex cases may not be handled) +> You'd still need to run the tests with deprecation helper enabled to ensure everything is fixed +> and then fix all deprecations left. + +> [!TIP] +> You can try to run twice these rules. Sometimes, the second run will find some difference that it could not spot on +> the first run. + +> [!NOTE] +> Once you've finished the migration to 2.0, it is not necessary anymore to keep the Foundry's rule set in your Rector +> config. + +### Doctrine's mapping + +Rector rules need to understand your Doctrine mapping to guess which one of `PersistentProxyObjectFactory` or +`ObjectFactory` it should use. + +If your mapping is defined in the code thanks to attributes or annotations, everything is OK. +If the mapping is defined outside the code, with xml, yaml or php configuration, some extra work is needed: + +1. Create a `tests/object-manager.php` file which will expose your doctrine config. Here is an example: +```php +use App\Kernel; +use Symfony\Component\Dotenv\Dotenv; + +require __DIR__ . '/../vendor/autoload.php'; + +(new Dotenv())->bootEnv(__DIR__ . '/../.env'); + +$kernel = new Kernel($_SERVER['APP_ENV'], (bool) $_SERVER['APP_DEBUG']); +$kernel->boot(); +return $kernel->getContainer()->get('doctrine')->getManager(); +``` + +2. Provide this file path to Rector's config: + +```php +paths(['tests']); // add all paths where Foundry is used + $rectorConfig->sets([FoundrySetList::UP_TO_FOUNDRY_2]); + $rectorConfig->singleton( + PersistenceResolver::class, + static fn() => new PersistenceResolver(__DIR__ . '/tests/object-manager.php') + ); +}; +``` + +## Known BC breaks + +The following error cannot be covered by Rector rules nor by the deprecation layer. +``` +RefreshObjectFailed: Cannot auto refresh "[Entity FQCN]" as there are unsaved changes. +Be sure to call ->_save() or disable auto refreshing. +``` + +This is an auto-refresh problem, where the "proxified" object is being accessed after modification. +You'll find some [documentation](https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#auto-refresh) +about it. + +To mitigate this problem, you should either stop using a proxy object, or wrap the modifications in the method +`->_withoutAutoRefresh()`. + +```diff +$proxyPost = PostProxyFactory::createOne(); +- $proxyPost->setTitle(); +- $proxyPost->setBody(); // 💥 ++$proxyPost->_withoutAutoRefresh( ++ function(Post $object) { ++ $proxyPost->setTitle(); ++ $proxyPost->setBody(); ++ } ++); +``` + + +## Deprecations list + +Here is the full list of modifications needed: + +### Factory + +- `withAttributes()` and `addState()` are both deprecated in favor of `with()` +- `sequence()` and `createSequence()` do not accept `callable` as a parameter anymore + +#### Change factories' base class + +`Zenstruck\Foundry\ModelFactory` is now deprecated. +You should choose between: +- `\Zenstruck\Foundry\ObjectFactory`: creates not-persistent plain objects, +- `\Zenstruck\Foundry\Persistence\PersistentObjectFactory`: creates and stores persisted objects, and directly return them, +- `\Zenstruck\Foundry\Persistence\PersistentProxyObjectFactory`: same as above, but returns a "proxy" version of the object. + This last class basically acts the same way as the old `ModelFactory`. + +As a rule of thumb to help you to choose between these two new factory parent classes: +- using `ObjectFactory` is straightforward: if the object cannot be persisted, you must use this one +- only entities (ORM) or documents (ODM) should use `PersistentObjectFactory` or `PersistentProxyObjectFactory` +- you should only use `PersistentProxyObjectFactory` if you want to leverage "auto refresh" behavior + +> [!WARNING] +> nor `PersistentObjectFactory` or `PersistentProxyObjectFactory` should be chosen to create not persistent objects. +> This will throw a deprecation in 1.x and will create an error in 2.0 + +> [!IMPORTANT] +> Since `PersistentObjectFactory` does not return a `Proxy` anymore, you'll have to remove all calls to `->object()` +> or any other proxy method on object created by this type of factory. + +> [!NOTE] +> You will have to change some methods prototypes in your classes: + +```php +// before +protected function getDefaults(): array +{ + // ... +} + +// after +protected function defaults(): array|callable +{ + // ... +} +``` + +```php +// before +protected static function getClass(): string +{ + // ... +} + +// after +public static function class(): string +{ + // ... +} +``` + +```php +// before +protected function initialize() +{ + // ... +} + +// after +protected function initialize(); static +{ + // ... +} +``` + +### Proxy + +Foundry 2.0 will completely change how `Proxy` system works, by leveraging Symfony's lazy proxy mechanism. +`Proxy` won't be anymore a wrapper class, but a "real" proxy, meaning your objects will be of the desired class AND `Proxy` object. +This implies that calling `->object()` (or, now, `_real()`) everywhere to satisfy the type system won't be needed anymore! + +`Proxy` class comes with deprecations as well: +- replace everywhere you're type-hinting `Zenstruck\Foundry\Proxy` to the interface `Zenstruck\Foundry\Persistence\Proxy` +- most of `Proxy` methods are deprecated: + - `object()` -> `_real()` + - `save()` -> `_save()` + - `remove()` -> `_delete()` + - `refresh()` -> `_refresh()` + - `forceSet()` -> `_set()` + - `forceGet()` -> `_get()` + - `repository()` -> `_repository()` + - `enableAutoRefresh()` -> `_enableAutoRefresh()` + - `disableAutoRefresh()` -> `_disableAutoRefresh()` + - `withoutAutoRefresh()` -> `_withoutAutoRefresh()` + - `isPersisted()` is removed without any replacement + - `forceSetAll()` is removed without any replacement + - `assertPersisted()` is removed without any replacement + - `assertNotPersisted()` is removed without any replacement +- Everywhere you've type-hinted `Zenstruck\Foundry\FactoryCollection` which was coming from a `PersistentProxyObjectFactory`, replace to `Zenstruck\Foundry\FactoryCollection>` + +### Instantiator + +- `Zenstruck\Foundry\Instantiator` class is deprecated in favor of `\Zenstruck\Foundry\Object\Instantiator`. You should change them everywhere. +- `new Instantiator()` is deprecated: use `Instantiator::withConstructor()` or `Instantiator::withoutConstructor()` depending on your needs. +- `Instantiator::alwaysForceProperties()` is deprecated in favor of `Instantiator::alwaysForce()`. Be careful of the modification of the parameter which is now a variadic. +- `Instantiator::allowExtraAttributes()` is deprecated in favor of `Instantiator::allowExtra()`. Be careful of the modification of the parameter which is now a variadic. +- Configuration `zenstruck_foundry.without_constructor` is deprecated in favor of `zenstruck_foundry.use_constructor` + +### Standalone functions + +- `Zenstruck\Foundry\create()` -> `Zenstruck\Foundry\Persistence\persist()` +- `Zenstruck\Foundry\instantiate()` -> `Zenstruck\Foundry\object()` +- `Zenstruck\Foundry\repository()` -> `Zenstruck\Foundry\Persistence\repository()` +- `Zenstruck\Foundry\Factory::delayFlush()` -> `Zenstruck\Foundry\Persistence\flush_after()` +- Usage of any method in `Zenstruck\Foundry\Test\TestState` should be replaced by `Zenstruck\Foundry\Test\UnitTestConfig::configure()` +- `Zenstruck\Foundry\instantiate_many()` is removed without any replacement +- `Zenstruck\Foundry\create_many()` is removed without any replacement + +### Trait `Factories` +- `Factories::disablePersist()` -> `Zenstruck\Foundry\Persistence\disable_persisting()` +- `Factories::enablePersist()` -> `Zenstruck\Foundry\Persistence\enable_persisting()` +- both `disablePersist()` and `enable_persisting()` should not be called when Foundry is booted without Doctrine (ie: in a unit test) + +### Bundle configuration + +Here is a diff of the bundle's configuration, all configs in red should be migrated to the green ones: + +```diff +zenstruck_foundry: +- auto_refresh_proxies: null + instantiator: +- without_constructor: false ++ use_constructor: true ++ orm: ++ auto_persist: true ++ reset: ++ connections: [default] ++ entity_managers: [default] ++ mode: schema ++ mongo: ++ auto_persist: true ++ reset: ++ document_managers: [default] +- database_resetter: +- enabled: true +- orm: +- connections: [] +- object_managers: [] +- reset_mode: schema +- odm: +- object_managers: [] +``` + +### Misc. +- type-hinting to `Zenstruck\Foundry\RepositoryProxy` should be replaced by `Zenstruck\Foundry\Persistence\RepositoryDecorator` +- type-hinting to `Zenstruck\Foundry\RepositoryAssertions` should be replaced by `Zenstruck\Foundry\Persistence\RepositoryAssertions` +- Methods in `Zenstruck\Foundry\RepositoryProxy` do not return `Proxy` anymore, but they return the actual object + + + diff --git a/bin/tools/php-cs-fixer/composer.json b/bin/tools/php-cs-fixer/composer.json new file mode 100644 index 000000000..779b6ebc6 --- /dev/null +++ b/bin/tools/php-cs-fixer/composer.json @@ -0,0 +1,5 @@ +{ + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.49" + } +} diff --git a/bin/tools/phpstan/composer.lock b/bin/tools/phpstan/composer.lock index 4bcdb402a..a3e96d8e7 100644 --- a/bin/tools/phpstan/composer.lock +++ b/bin/tools/phpstan/composer.lock @@ -117,16 +117,16 @@ }, { "name": "phpstan/phpstan", - "version": "1.10.50", + "version": "1.10.62", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "06a98513ac72c03e8366b5a0cb00750b487032e4" + "reference": "cd5c8a1660ed3540b211407c77abf4af193a6af9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/06a98513ac72c03e8366b5a0cb00750b487032e4", - "reference": "06a98513ac72c03e8366b5a0cb00750b487032e4", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/cd5c8a1660ed3540b211407c77abf4af193a6af9", + "reference": "cd5c8a1660ed3540b211407c77abf4af193a6af9", "shasum": "" }, "require": { @@ -175,20 +175,20 @@ "type": "tidelift" } ], - "time": "2023-12-13T10:59:42+00:00" + "time": "2024-03-13T12:27:20+00:00" }, { "name": "phpstan/phpstan-symfony", - "version": "1.3.5", + "version": "1.3.8", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-symfony.git", - "reference": "27ff6339f83796a7e0dd963cf445cd3c456fc620" + "reference": "d8a0bc03a68d95288b6471c37d435647fbdaff1a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-symfony/zipball/27ff6339f83796a7e0dd963cf445cd3c456fc620", - "reference": "27ff6339f83796a7e0dd963cf445cd3c456fc620", + "url": "https://api.github.com/repos/phpstan/phpstan-symfony/zipball/d8a0bc03a68d95288b6471c37d435647fbdaff1a", + "reference": "d8a0bc03a68d95288b6471c37d435647fbdaff1a", "shasum": "" }, "require": { @@ -245,9 +245,9 @@ "description": "Symfony Framework extensions and rules for PHPStan", "support": { "issues": "https://github.com/phpstan/phpstan-symfony/issues", - "source": "https://github.com/phpstan/phpstan-symfony/tree/1.3.5" + "source": "https://github.com/phpstan/phpstan-symfony/tree/1.3.8" }, - "time": "2023-10-30T14:52:15+00:00" + "time": "2024-03-05T16:33:08+00:00" } ], "packages-dev": [], diff --git a/composer.json b/composer.json index efc9b4d85..84d04c157 100644 --- a/composer.json +++ b/composer.json @@ -22,7 +22,7 @@ "zenstruck/callback": "^1.1" }, "require-dev": { - "bamarni/composer-bin-plugin": "^1.4", + "bamarni/composer-bin-plugin": "^1.8", "dama/doctrine-test-bundle": "^7.0|^8.0", "doctrine/doctrine-bundle": "^2.5", "doctrine/doctrine-migrations-bundle": "^2.2|^3.0", @@ -50,8 +50,14 @@ "doctrine/mongodb-odm": "2.5.0" }, "autoload": { - "psr-4": { "Zenstruck\\Foundry\\": "src/" }, - "files": ["src/functions.php"] + "psr-4": { + "Zenstruck\\Foundry\\": "src/", + "Zenstruck\\Foundry\\Utils\\Rector\\": "utils/rector/src/" + }, + "files": [ + "src/functions.php", + "src/Persistence/functions.php" + ] }, "autoload-dev": { "psr-4": { diff --git a/docs/index.rst b/docs/index.rst index 426ac18a6..5b57635d8 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -396,9 +396,6 @@ You can use states to make your tests very explicit to improve readability: ->create() ; - // states that don't require arguments can be added as strings to PostFactory::new() - $post = PostFactory::new('published', 'withViewCount')->create(); - .. note:: Be sure to chain the states/hooks off of ``$this`` because factories are `Immutable`_. diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index fbeb585ed..81436056c 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1,15 +1,35 @@ parameters: ignoreErrors: - - message: "#^Parameter \\#1 \\$objectOrClass of class ReflectionClass constructor expects class\\-string\\\\|object, class\\-string\\|null given\\.$#" + message: "#^Should not use node with type \"Expr_Eval\", please change the code\\.$#" count: 1 - path: src/Bundle/DependencyInjection/GlobalStatePass.php + path: src/AnonymousFactoryGenerator.php + + - + message: "#^Method Zenstruck\\\\Foundry\\\\Configuration\\:\\:repositoryFor\\(\\) should return Zenstruck\\\\Foundry\\\\Persistence\\\\RepositoryDecorator\\ but returns Zenstruck\\\\Foundry\\\\Persistence\\\\ProxyRepositoryDecorator\\\\|Zenstruck\\\\Foundry\\\\Persistence\\\\RepositoryDecorator\\\\.$#" + count: 1 + path: src/Configuration.php - message: "#^Parameter \\#1 \\$class of method Doctrine\\\\Persistence\\\\ManagerRegistry\\:\\:getManagerForClass\\(\\) expects class\\-string, string given\\.$#" count: 1 path: src/Configuration.php + - + message: "#^Method Zenstruck\\\\Foundry\\\\Factory\\:\\:createAndUproxify\\(\\) should return TObject of object but returns object\\.$#" + count: 1 + path: src/Factory.php + + - + message: "#^Should not use function \"debug_backtrace\", please change the code\\.$#" + count: 2 + path: src/Factory.php + + - + message: "#^Method Zenstruck\\\\Foundry\\\\FactoryCollection\\:\\:create\\(\\) should return array\\ but returns array\\, \\(TObject of object\\)\\|Zenstruck\\\\Foundry\\\\Persistence\\\\Proxy\\\\>\\.$#" + count: 1 + path: src/FactoryCollection.php + - message: "#^Parameter \\#1 \\$min of function random_int expects int, int\\|null given\\.$#" count: 1 @@ -21,7 +41,82 @@ parameters: path: src/FactoryCollection.php - - message: "#^Method Zenstruck\\\\Foundry\\\\RepositoryProxy\\:\\:proxyResult\\(\\) should return array\\\\>\\|Zenstruck\\\\Foundry\\\\Proxy\\ but returns \\(TProxiedObject of object\\)\\|null\\.$#" + message: "#^Should not use function \"debug_backtrace\", please change the code\\.$#" + count: 1 + path: src/FactoryCollection.php + + - + message: "#^Class Zenstruck\\\\Foundry\\\\Instantiator extends @final class Zenstruck\\\\Foundry\\\\Object\\\\Instantiator\\.$#" + count: 1 + path: src/Instantiator.php + + - + message: "#^If condition is always false\\.$#" + count: 1 + path: src/Instantiator.php + + - + message: "#^Method Zenstruck\\\\Foundry\\\\ObjectFactory\\:\\:__callStatic\\(\\) should return array\\ but returns array\\\\>\\.$#" + count: 1 + path: src/ObjectFactory.php + + - + message: "#^Method Zenstruck\\\\Foundry\\\\ObjectFactory\\:\\:createSequence\\(\\) should return array\\ but returns array\\\\>\\.$#" + count: 1 + path: src/ObjectFactory.php + + - + message: "#^Should not use function \"debug_backtrace\", please change the code\\.$#" + count: 1 + path: src/ObjectFactory.php + + - + message: "#^Unsafe usage of new static\\(\\)\\.$#" + count: 1 + path: src/ObjectFactory.php + + - + message: "#^Should not use function \"debug_backtrace\", please change the code\\.$#" + count: 1 + path: src/Persistence/PersistentProxyObjectFactory.php + + - + message: "#^Class Zenstruck\\\\Foundry\\\\Persistence\\\\ProxyRepositoryDecorator extends @final class Zenstruck\\\\Foundry\\\\Persistence\\\\RepositoryDecorator\\.$#" + count: 1 + path: src/Persistence/ProxyRepositoryDecorator.php + + - + message: "#^Method Zenstruck\\\\Foundry\\\\Persistence\\\\ProxyRepositoryDecorator\\:\\:proxyResult\\(\\) should return array\\\\>\\|Zenstruck\\\\Foundry\\\\Persistence\\\\Proxy\\ but returns \\(TProxiedObject of object\\)\\|null\\.$#" + count: 1 + path: src/Persistence/ProxyRepositoryDecorator.php + + - + message: "#^Method Zenstruck\\\\Foundry\\\\Persistence\\\\ProxyRepositoryDecorator\\:\\:proxyResult\\(\\) should return array\\\\>\\|Zenstruck\\\\Foundry\\\\Persistence\\\\Proxy\\ but returns Zenstruck\\\\Foundry\\\\Proxy\\.$#" + count: 1 + path: src/Persistence/ProxyRepositoryDecorator.php + + - + message: "#^PHPDoc tag @return with type Zenstruck\\\\Foundry\\\\Persistence\\\\Proxy\\ is not subtype of native type Zenstruck\\\\Foundry\\\\Proxy\\.$#" + count: 1 + path: src/Proxy.php + + - + message: "#^Class Zenstruck\\\\Foundry\\\\RepositoryAssertions extends @final class Zenstruck\\\\Foundry\\\\Persistence\\\\RepositoryAssertions\\.$#" + count: 1 + path: src/RepositoryAssertions.php + + - + message: "#^If condition is always false\\.$#" + count: 1 + path: src/RepositoryAssertions.php + + - + message: "#^Class Zenstruck\\\\Foundry\\\\RepositoryProxy extends @final class Zenstruck\\\\Foundry\\\\Persistence\\\\ProxyRepositoryDecorator\\.$#" + count: 1 + path: src/RepositoryProxy.php + + - + message: "#^If condition is always false\\.$#" count: 1 path: src/RepositoryProxy.php @@ -54,8 +149,3 @@ parameters: message: "#^Parameter \\#1 \\$configuration of static method Zenstruck\\\\Foundry\\\\Factory\\\\:\\:boot\\(\\) expects Zenstruck\\\\Foundry\\\\Configuration, object\\|null given\\.$#" count: 1 path: src/ZenstruckFoundryBundle.php - - - - message: "#^Function Zenstruck\\\\Foundry\\\\anonymous\\(\\) should return Zenstruck\\\\Foundry\\\\Factory\\ but returns class@anonymous/src/functions\\.php\\:45\\.$#" - count: 1 - path: src/functions.php diff --git a/phpunit.dama.xml.dist b/phpunit.dama.xml.dist index 7eb64aa3f..09054936d 100644 --- a/phpunit.dama.xml.dist +++ b/phpunit.dama.xml.dist @@ -11,7 +11,7 @@ - + diff --git a/psalm.xml b/psalm.xml index fe40b7965..f6647a3ff 100644 --- a/psalm.xml +++ b/psalm.xml @@ -12,4 +12,9 @@ + + + + + diff --git a/src/AnonymousFactory.php b/src/AnonymousFactory.php index ed749a03a..28c203cfe 100644 --- a/src/AnonymousFactory.php +++ b/src/AnonymousFactory.php @@ -11,6 +11,11 @@ namespace Zenstruck\Foundry; +use Zenstruck\Foundry\Persistence\Proxy; +use Zenstruck\Foundry\Persistence\ProxyRepositoryDecorator; +use Zenstruck\Foundry\Persistence\RepositoryAssertions; +use Zenstruck\Foundry\Persistence\RepositoryDecorator; + /** * @template TModel of object * @template-extends Factory @@ -21,8 +26,15 @@ */ final class AnonymousFactory extends Factory implements \Countable, \IteratorAggregate { + /** + * @var class-string + */ + private string $class; + public function __construct(string $class, array|callable $defaultAttributes = []) { + $this->class = $class; + trigger_deprecation('zenstruck\foundry', '1.30', 'Class "AnonymousFactory" is deprecated and will be removed in 2.0. Use the "anonymous()" or "repository()" functions instead.'); parent::__construct($class, $defaultAttributes); @@ -55,35 +67,35 @@ public function findOrCreate(array $attributes): Proxy } /** - * @see RepositoryProxy::first() + * @see RepositoryDecorator::first() * * @throws \RuntimeException If no entities exist */ public function first(string $sortedField = 'id'): Proxy { if (null === $proxy = $this->repository()->first($sortedField)) { - throw new \RuntimeException(\sprintf('No "%s" objects persisted.', $this->class())); + throw new \RuntimeException(\sprintf('No "%s" objects persisted.', $this->class)); } return $proxy; } /** - * @see RepositoryProxy::last() + * @see RepositoryDecorator::last() * * @throws \RuntimeException If no entities exist */ public function last(string $sortedField = 'id'): Proxy { if (null === $proxy = $this->repository()->last($sortedField)) { - throw new \RuntimeException(\sprintf('No "%s" objects persisted.', $this->class())); + throw new \RuntimeException(\sprintf('No "%s" objects persisted.', $this->class)); } return $proxy; } /** - * @see RepositoryProxy::random() + * @see RepositoryDecorator::random() */ public function random(array $attributes = []): Proxy { @@ -106,7 +118,7 @@ public function randomOrCreate(array $attributes = []): Proxy } /** - * @see RepositoryProxy::randomSet() + * @see RepositoryDecorator::randomSet() * * @return object[] */ @@ -116,7 +128,7 @@ public function randomSet(int $number, array $attributes = []): array } /** - * @see RepositoryProxy::randomRange() + * @see RepositoryDecorator::randomRange() * * @return object[] */ @@ -126,7 +138,7 @@ public function randomRange(int $min, int $max, array $attributes = []): array } /** - * @see RepositoryProxy::count() + * @see RepositoryDecorator::count() */ public function count(): int { @@ -139,7 +151,7 @@ public function getIterator(): \ArrayIterator } /** - * @see RepositoryProxy::truncate() + * @see RepositoryDecorator::truncate() */ public function truncate(): void { @@ -147,7 +159,7 @@ public function truncate(): void } /** - * @see RepositoryProxy::findAll() + * @see RepositoryDecorator::findAll() * * @return object[] */ @@ -157,7 +169,7 @@ public function all(): array } /** - * @see RepositoryProxy::find() + * @see RepositoryDecorator::find() * * @phpstan-param Proxy|array|mixed $criteria * @@ -166,14 +178,14 @@ public function all(): array public function find($criteria): Proxy { if (null === $proxy = $this->repository()->find($criteria)) { - throw new \RuntimeException(\sprintf('Could not find "%s" object.', $this->class())); + throw new \RuntimeException(\sprintf('Could not find "%s" object.', $this->class)); } return $proxy; } /** - * @see RepositoryProxy::findBy() + * @see RepositoryDecorator::findBy() * * @return object[] */ @@ -188,10 +200,10 @@ public function assert(): RepositoryAssertions } /** - * @phpstan-return RepositoryProxy + * @phpstan-return ProxyRepositoryDecorator */ - public function repository(): RepositoryProxy + public function repository(): ProxyRepositoryDecorator { - return self::configuration()->repositoryFor($this->class()); + return self::configuration()->repositoryFor($this->class, proxy: true); } } diff --git a/src/AnonymousFactoryGenerator.php b/src/AnonymousFactoryGenerator.php new file mode 100644 index 000000000..fee4b2520 --- /dev/null +++ b/src/AnonymousFactoryGenerator.php @@ -0,0 +1,61 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry; + +/** + * @author Kevin Bond + * + * @internal + */ +final class AnonymousFactoryGenerator +{ + /** + * @template T of object + * @template F of Factory + * + * @param class-string $class + * @param class-string $factoryClass + * + * @return class-string + */ + public static function create(string $class, string $factoryClass): string + { + $anonymousClassName = \sprintf('FoundryAnonymous%s_', (new \ReflectionClass($factoryClass))->getShortName()); + $anonymousClassName .= \str_replace('\\', '', $class); + $anonymousClassName = \preg_replace('/\W/', '', $anonymousClassName); // sanitize for anonymous classes + + /** @var class-string $anonymousClassName */ + if (!\class_exists($anonymousClassName)) { + $anonymousClassCode = <<addDefaultsIfNotSet() ->info('Configure the default instantiator used by your factories.') ->validate() - ->ifTrue(static fn(array $v) => $v['service'] && $v['without_constructor']) - ->thenInvalid('Cannot set "without_constructor" when using custom service.') + ->ifTrue(static fn(array $v) => $v['service'] && (($v['without_constructor'] ?? false) || !($v['use_constructor'] ?? true))) + ->thenInvalid('Cannot set "use_constructor: false" when using custom service.') ->end() ->validate() ->ifTrue(static fn(array $v) => $v['service'] && $v['allow_extra_attributes']) @@ -76,8 +76,13 @@ public function getConfigTreeBuilder(): TreeBuilder ->end() ->children() ->booleanNode('without_constructor') - ->defaultFalse() ->info('Whether or not to call an object\'s constructor during instantiation.') + ->setDeprecated('zenstruck/foundry', '1.38.0', 'Configuration "zenstruck_foundry.instantiator.without_constructor" is deprecated and will be removed in 2.0. Use "zenstruck_foundry.instantiator.use_constructor" instead.') + ->end() + ->booleanNode('use_constructor') + ->info('Use the constructor to instantiate objects.') + // default is set in ZenstruckFoundryExtension till we have deprecation layer + // ->defaultTrue() ->end() ->booleanNode('allow_extra_attributes') ->defaultFalse() @@ -94,7 +99,50 @@ public function getConfigTreeBuilder(): TreeBuilder ->end() ->end() ->end() + ->arrayNode('orm') + // could be activated in 2.0 + // ->addDefaultsIfNotSet() + ->children() + ->arrayNode('reset') + ->addDefaultsIfNotSet() + ->children() + ->arrayNode('connections') + ->info('DBAL connections to reset with ResetDatabase trait') + ->defaultValue(['default']) + ->scalarPrototype()->end() + ->end() + ->arrayNode('entity_managers') + ->info('Entity Managers to reset with ResetDatabase trait') + ->defaultValue(['default']) + ->scalarPrototype()->end() + ->end() + ->enumNode('mode') + ->info('Reset mode to use with ResetDatabase trait') + ->defaultValue(ORMDatabaseResetter::RESET_MODE_SCHEMA) + ->values([ORMDatabaseResetter::RESET_MODE_SCHEMA, ORMDatabaseResetter::RESET_MODE_MIGRATE]) + ->end() + ->end() + ->end() + ->end() + ->end() + ->arrayNode('mongo') + // could be activated in 2.0 + // ->addDefaultsIfNotSet() + ->children() + ->arrayNode('reset') + ->addDefaultsIfNotSet() + ->children() + ->arrayNode('document_managers') + ->info('Document Managers to reset with ResetDatabase trait') + ->defaultValue(['default']) + ->scalarPrototype()->end() + ->end() + ->end() + ->end() + ->end() + ->end() ->arrayNode('database_resetter') + ->setDeprecated('zenstruck/foundry', '1.38.0', 'Configuration "zenstruck_foundry.database_resetter" is deprecated and will be removed in 2.0. Use "zenstruck_foundry.orm.reset" or "zenstruck_foundry.mongo.reset" instead.') ->canBeDisabled() ->addDefaultsIfNotSet() ->info('Configure database reset mechanism.') diff --git a/src/Bundle/DependencyInjection/GlobalStatePass.php b/src/Bundle/DependencyInjection/GlobalStatePass.php index a237190b9..62a3dcb2a 100644 --- a/src/Bundle/DependencyInjection/GlobalStatePass.php +++ b/src/Bundle/DependencyInjection/GlobalStatePass.php @@ -69,7 +69,7 @@ private function isInvokableService(ContainerBuilder $container, string $globalS $globalStateItemDefinition = $container->getDefinition($globalStateItem); - return (new \ReflectionClass($globalStateItemDefinition->getClass()))->hasMethod('__invoke'); + return (new \ReflectionClass($globalStateItemDefinition->getClass()))->hasMethod('__invoke'); // @phpstan-ignore-line } private function isStandaloneStory(ContainerBuilder $container, string $globalStateItem): bool diff --git a/src/Bundle/DependencyInjection/ZenstruckFoundryExtension.php b/src/Bundle/DependencyInjection/ZenstruckFoundryExtension.php index 993c90828..09d2c3422 100644 --- a/src/Bundle/DependencyInjection/ZenstruckFoundryExtension.php +++ b/src/Bundle/DependencyInjection/ZenstruckFoundryExtension.php @@ -22,9 +22,12 @@ use Symfony\Component\HttpKernel\DependencyInjection\ConfigurableExtension; use Zenstruck\Foundry\Bundle\Command\StubMakeFactory; use Zenstruck\Foundry\Bundle\Command\StubMakeStory; -use Zenstruck\Foundry\ModelFactory; +use Zenstruck\Foundry\Object\Instantiator; +use Zenstruck\Foundry\Persistence\PersistentObjectFactory; +use Zenstruck\Foundry\Persistence\PersistentProxyObjectFactory; use Zenstruck\Foundry\Story; use Zenstruck\Foundry\Test\ORMDatabaseResetter; +use Zenstruck\Foundry\Test\ResetDatabase; /** * @author Kevin Bond @@ -41,18 +44,29 @@ protected function loadInternal(array $mergedConfig, ContainerBuilder $container ->addTag('foundry.story') ; - $container->registerForAutoconfiguration(ModelFactory::class) + $container->registerForAutoconfiguration(PersistentProxyObjectFactory::class) ->addTag('foundry.factory') ; $this->configureFaker($mergedConfig['faker'], $container); $this->configureDefaultInstantiator($mergedConfig['instantiator'], $container); - $this->configureDatabaseResetter($mergedConfig['database_resetter'], $container); + $this->configureDatabaseResetter($mergedConfig, $container); $this->configureMakeFactory($mergedConfig['make_factory'], $container, $loader); if (true === $mergedConfig['auto_refresh_proxies']) { $container->getDefinition('.zenstruck_foundry.configuration')->addMethodCall('enableDefaultProxyAutoRefresh'); } elseif (false === $mergedConfig['auto_refresh_proxies']) { + trigger_deprecation( + 'zenstruck\foundry', + '1.38.0', + <<getDefinition('.zenstruck_foundry.configuration')->addMethodCall('disableDefaultProxyAutoRefresh'); } } @@ -86,16 +100,23 @@ private function configureDefaultInstantiator(array $config, ContainerBuilder $c $definition = $container->getDefinition('.zenstruck_foundry.default_instantiator'); - if ($config['without_constructor']) { - $definition->addMethodCall('withoutConstructor'); + if (isset($config['without_constructor']) + && isset($config['use_constructor']) + && $config['without_constructor'] === $config['use_constructor'] + ) { + throw new \InvalidArgumentException('Cannot set "without_constructor" and "use_constructor" to the same value.'); } + $withoutConstructor = $config['without_constructor'] ?? !($config['use_constructor'] ?? true); + + $definition->setFactory([Instantiator::class, $withoutConstructor ? 'withoutConstructor' : 'withConstructor']); + if ($config['allow_extra_attributes']) { - $definition->addMethodCall('allowExtraAttributes'); + $definition->addMethodCall('allowExtra'); } if ($config['always_force_properties']) { - $definition->addMethodCall('alwaysForceProperties'); + $definition->addMethodCall('alwaysForce'); } } @@ -103,22 +124,34 @@ private function configureDatabaseResetter(array $config, ContainerBuilder $cont { $configurationDefinition = $container->getDefinition('.zenstruck_foundry.configuration'); - if (false === $config['enabled']) { + $legacyConfig = $config['database_resetter']; + + if (false === $legacyConfig['enabled']) { + trigger_deprecation('zenstruck\foundry', '1.38.0', 'Disabling database reset via bundle configuration is deprecated and will be removed in 2.0. Instead you should not use "%s" trait in your test.', ResetDatabase::class); + $configurationDefinition->addMethodCall('disableDatabaseReset'); } - if (isset($config['orm']) && !self::isBundleLoaded($container, DoctrineBundle::class)) { - throw new \InvalidArgumentException('doctrine/doctrine-bundle should be enabled to use config under "database_resetter.orm".'); + if (isset($legacyConfig['orm']) && isset($config['orm']['reset'])) { + throw new \InvalidArgumentException('Configurations "zenstruck_foundry.orm.reset" and "zenstruck_foundry.database_resetter.orm" are incompatible. You should only use "zenstruck_foundry.orm.reset".'); + } + + if ((isset($legacyConfig['orm']) || isset($config['orm']['reset'])) && !self::isBundleLoaded($container, DoctrineBundle::class)) { + throw new \InvalidArgumentException('doctrine/doctrine-bundle should be enabled to use config under "orm.reset".'); + } + + if (isset($legacyConfig['odm']) && isset($config['mongo']['reset'])) { + throw new \InvalidArgumentException('Configurations "zenstruck_foundry.mongo.reset" and "zenstruck_foundry.database_resetter.odm" are incompatible. You should only use "zenstruck_foundry.mongo.reset".'); } - if (isset($config['odm']) && !self::isBundleLoaded($container, DoctrineMongoDBBundle::class)) { - throw new \InvalidArgumentException('doctrine/mongodb-odm-bundle should be enabled to use config under "database_resetter.odm".'); + if ((isset($legacyConfig['odm']) || isset($config['mongo']['reset'])) && !self::isBundleLoaded($container, DoctrineMongoDBBundle::class)) { + throw new \InvalidArgumentException('doctrine/mongodb-odm-bundle should be enabled to use config under "mongo.reset".'); } - $configurationDefinition->setArgument('$ormConnectionsToReset', $config['orm']['connections'] ?? []); - $configurationDefinition->setArgument('$ormObjectManagersToReset', $config['orm']['object_managers'] ?? []); - $configurationDefinition->setArgument('$ormResetMode', $config['orm']['reset_mode'] ?? ORMDatabaseResetter::RESET_MODE_SCHEMA); - $configurationDefinition->setArgument('$odmObjectManagersToReset', $config['odm']['object_managers'] ?? []); + $configurationDefinition->setArgument('$ormConnectionsToReset', $legacyConfig['orm']['connections'] ?? $config['orm']['reset']['connections'] ?? ['default']); + $configurationDefinition->setArgument('$ormObjectManagersToReset', $legacyConfig['orm']['object_managers'] ?? $config['orm']['reset']['entity_managers'] ?? ['default']); + $configurationDefinition->setArgument('$ormResetMode', $legacyConfig['orm']['reset_mode'] ?? $config['orm']['reset']['mode'] ?? ORMDatabaseResetter::RESET_MODE_SCHEMA); + $configurationDefinition->setArgument('$odmObjectManagersToReset', $legacyConfig['odm']['object_managers'] ?? $config['mongo']['reset']['document_managers'] ?? ['default']); } private function configureMakeFactory(array $makerConfig, ContainerBuilder $container, FileLoader $loader): void diff --git a/src/Bundle/Maker/Factory/FactoryClassMap.php b/src/Bundle/Maker/Factory/FactoryClassMap.php index d8b91dfbc..a3fc1b3a0 100644 --- a/src/Bundle/Maker/Factory/FactoryClassMap.php +++ b/src/Bundle/Maker/Factory/FactoryClassMap.php @@ -12,7 +12,7 @@ namespace Zenstruck\Foundry\Bundle\Maker\Factory; use Zenstruck\Foundry\Bundle\Maker\Factory\Exception\FactoryClassAlreadyExistException; -use Zenstruck\Foundry\ModelFactory; +use Zenstruck\Foundry\Persistence\PersistentProxyObjectFactory; /** * @internal @@ -24,14 +24,14 @@ final class FactoryClassMap */ private array $classesWithFactories; - /** @param \Traversable $factories */ + /** @param \Traversable $factories */ public function __construct(\Traversable $factories) { $this->classesWithFactories = \array_unique( \array_reduce( \iterator_to_array($factories, preserve_keys: true), - static function(array $carry, ModelFactory $factory): array { - $carry[$factory::class] = $factory::getEntityClass(); + static function(array $carry, PersistentProxyObjectFactory $factory): array { + $carry[$factory::class] = $factory::class(); return $carry; }, diff --git a/src/Bundle/Maker/Factory/MakeFactoryData.php b/src/Bundle/Maker/Factory/MakeFactoryData.php index f48303986..a67cde763 100644 --- a/src/Bundle/Maker/Factory/MakeFactoryData.php +++ b/src/Bundle/Maker/Factory/MakeFactoryData.php @@ -11,11 +11,15 @@ namespace Zenstruck\Foundry\Bundle\Maker\Factory; +use Doctrine\ODM\MongoDB\Repository\DocumentRepository; +use Doctrine\ORM\EntityRepository; use Symfony\Bundle\MakerBundle\Str; use Symfony\Bundle\MakerBundle\Util\ClassNameDetails; -use Zenstruck\Foundry\ModelFactory; -use Zenstruck\Foundry\Proxy; -use Zenstruck\Foundry\RepositoryProxy; +use Zenstruck\Foundry\Factory; +use Zenstruck\Foundry\ObjectFactory; +use Zenstruck\Foundry\Persistence\PersistentProxyObjectFactory; +use Zenstruck\Foundry\Persistence\Proxy; +use Zenstruck\Foundry\Persistence\ProxyRepositoryDecorator; /** * @internal @@ -36,14 +40,20 @@ final class MakeFactoryData public function __construct(private \ReflectionClass $object, private ClassNameDetails $factoryClassNameDetails, private ?\ReflectionClass $repository, private string $staticAnalysisTool, private bool $persisted) { $this->uses = [ - ModelFactory::class, - Proxy::class, + $this->getFactoryClass(), $object->getName(), ]; + if ($this->persisted) { + $this->uses[] = Proxy::class; + } + if ($repository) { $this->uses[] = $repository->getName(); - $this->uses[] = RepositoryProxy::class; + $this->uses[] = ProxyRepositoryDecorator::class; + if (!\str_starts_with($repository->getName(), 'Doctrine')) { + $this->uses[] = \is_a($repository->getName(), DocumentRepository::class, allow_string: true) ? DocumentRepository::class : EntityRepository::class; + } } $this->methodsInPHPDoc = MakeFactoryPHPDocMethod::createAll($this); @@ -59,6 +69,19 @@ public function getObjectShortName(): string return $this->object->getShortName(); } + /** + * @return class-string + */ + public function getFactoryClass(): string + { + return $this->isPersisted() ? PersistentProxyObjectFactory::class : ObjectFactory::class; + } + + public function getFactoryClassShortName(): string + { + return (new \ReflectionClass($this->getFactoryClass()))->getShortName(); + } + public function getFactoryClassNameDetails(): ClassNameDetails { return $this->factoryClassNameDetails; @@ -70,9 +93,9 @@ public function getObjectFullyQualifiedClassName(): string return $this->object->getName(); } - public function getRepositoryShortName(): ?string + public function getRepositoryReflectionClass(): ?\ReflectionClass { - return $this->repository?->getShortName(); + return $this->repository; } public function isPersisted(): bool diff --git a/src/Bundle/Maker/Factory/MakeFactoryPHPDocMethod.php b/src/Bundle/Maker/Factory/MakeFactoryPHPDocMethod.php index e57e64544..208eacc5d 100644 --- a/src/Bundle/Maker/Factory/MakeFactoryPHPDocMethod.php +++ b/src/Bundle/Maker/Factory/MakeFactoryPHPDocMethod.php @@ -11,12 +11,14 @@ namespace Zenstruck\Foundry\Bundle\Maker\Factory; +use Doctrine\ODM\MongoDB\Repository\DocumentRepository; + /** * @internal */ final class MakeFactoryPHPDocMethod { - public function __construct(private string $objectName, private string $prototype, private bool $returnsCollection, private bool $isStatic = true, private ?string $repository = null) + public function __construct(private string $objectName, private string $prototype, private bool $returnsCollection, private bool $isStatic = true, private ?\ReflectionClass $repository = null) { } @@ -44,8 +46,8 @@ public static function createAll(MakeFactoryData $makeFactoryData): array $methods[] = new self($makeFactoryData->getObjectShortName(), 'randomRange(int $min, int $max, array $attributes = [])', returnsCollection: true); $methods[] = new self($makeFactoryData->getObjectShortName(), 'randomSet(int $number, array $attributes = [])', returnsCollection: true); - if (null !== $makeFactoryData->getRepositoryShortName()) { - $methods[] = new self($makeFactoryData->getObjectShortName(), 'repository()', returnsCollection: false, repository: $makeFactoryData->getRepositoryShortName()); + if (null !== $makeFactoryData->getRepositoryReflectionClass()) { + $methods[] = new self($makeFactoryData->getObjectShortName(), 'repository()', returnsCollection: false, repository: $makeFactoryData->getRepositoryReflectionClass()); } } @@ -59,14 +61,20 @@ public function toString(?string $staticAnalysisTool = null): string if ($this->repository) { $returnType = match ((bool) $staticAnalysisTool) { - false => "{$this->repository}|RepositoryProxy", - true => "RepositoryProxy<{$this->objectName}>", + false => "{$this->repository->getShortName()}|ProxyRepositoryDecorator", + true => \sprintf( + 'ProxyRepositoryDecorator<%s, %s>', + $this->objectName, + \is_a($this->repository->getName(), DocumentRepository::class, allow_string: true) + ? 'DocumentRepository' + : 'EntityRepository' + ), }; } else { $returnType = match ([$this->returnsCollection, (bool) $staticAnalysisTool]) { - [true, true] => "listobjectName}>>", + [true, true] => "list<{$this->objectName}&Proxy<{$this->objectName}>>", [true, false] => "{$this->objectName}[]|Proxy[]", - [false, true] => "Proxy<{$this->objectName}>", + [false, true] => "{$this->objectName}&Proxy<{$this->objectName}>", [false, false] => "{$this->objectName}|Proxy", }; } diff --git a/src/Bundle/Maker/Factory/NoPersistenceObjectsAutoCompleter.php b/src/Bundle/Maker/Factory/NoPersistenceObjectsAutoCompleter.php index 5f4c4ab27..2454c3387 100644 --- a/src/Bundle/Maker/Factory/NoPersistenceObjectsAutoCompleter.php +++ b/src/Bundle/Maker/Factory/NoPersistenceObjectsAutoCompleter.php @@ -34,6 +34,11 @@ public function getAutocompleteValues(): array $class = $this->toPSR4($rootPath, $phpFile, $namespacePrefix); + if (\in_array($class, ['Zenstruck\Foundry\Proxy', 'Zenstruck\Foundry\RepositoryProxy', 'Zenstruck\Foundry\RepositoryAssertions'])) { + // do not load legacy Proxy: prevents deprecations in tests. + continue; + } + try { // @phpstan-ignore-next-line $class is not always a class-string $reflection = new \ReflectionClass($class); diff --git a/src/Bundle/Resources/config/services.xml b/src/Bundle/Resources/config/services.xml index aef3fe9a2..6574a3ea6 100644 --- a/src/Bundle/Resources/config/services.xml +++ b/src/Bundle/Resources/config/services.xml @@ -5,7 +5,9 @@ https://symfony.com/schema/dic/services/services-1.0.xsd"> - + + + diff --git a/src/Bundle/Resources/skeleton/Factory.tpl.php b/src/Bundle/Resources/skeleton/Factory.tpl.php index e4dd60f98..0794b2476 100644 --- a/src/Bundle/Resources/skeleton/Factory.tpl.php +++ b/src/Bundle/Resources/skeleton/Factory.tpl.php @@ -9,23 +9,25 @@ ?> /** - * @extends ModelFactory<getObjectShortName(); ?>> - * + * @extends getFactoryClassShortName(); ?><getObjectShortName(); ?>> getMethodsPHPDoc() as $methodPHPDoc) { - echo "{$methodPHPDoc->toString()}\n"; -} - -if ($makeFactoryData->hasStaticAnalysisTool()) { +if ($makeFactoryData->isPersisted()) { echo " *\n"; - foreach ($makeFactoryData->getMethodsPHPDoc() as $methodPHPDoc) { - echo "{$methodPHPDoc->toString($makeFactoryData->staticAnalysisTool())}\n"; + echo "{$methodPHPDoc->toString()}\n"; + } + + if ($makeFactoryData->hasStaticAnalysisTool()) { + echo " *\n"; + + foreach ($makeFactoryData->getMethodsPHPDoc() as $methodPHPDoc) { + echo "{$methodPHPDoc->toString($makeFactoryData->staticAnalysisTool())}\n"; + } } } ?> */ -final class extends ModelFactory +final class extends getFactoryClassShortName(); ?> { /** * @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#factories-as-services @@ -34,7 +36,11 @@ final class extends ModelFactory */ public function __construct() { - parent::__construct(); + } + + public static function class(): string + { + return getObjectShortName(); ?>::class; } /** @@ -42,7 +48,7 @@ public function __construct() * * @todo add your default values here */ - protected function getDefaults(): array + protected function defaults(): array|callable { return [ isPersisted()) { ?> - ->withoutPersisting() - // ->afterInstantiate(function(getObjectShortName(); ?> $getObjectShortName()); ?>): void {}) ; } - - protected static function getClass(): string - { - return getObjectShortName(); ?>::class; - } } diff --git a/src/Configuration.php b/src/Configuration.php index d890e7a35..b2defebf4 100644 --- a/src/Configuration.php +++ b/src/Configuration.php @@ -16,6 +16,12 @@ use Doctrine\Persistence\ObjectManager; use Faker; use Zenstruck\Foundry\Exception\FoundryBootException; +use Zenstruck\Foundry\Persistence\PersistentObjectFactory; +use Zenstruck\Foundry\Persistence\PersistentProxyObjectFactory; +use Zenstruck\Foundry\Persistence\Proxy; +use Zenstruck\Foundry\Persistence\ProxyRepositoryDecorator; +use Zenstruck\Foundry\Persistence\RepositoryDecorator; +use Zenstruck\Foundry\Test\ResetDatabase; /** * @internal @@ -53,7 +59,7 @@ public function __construct(private array $ormConnectionsToReset, private array $this->stories = new StoryManager([]); $this->factories = new ModelFactoryManager([]); $this->faker = Faker\Factory::create(); - $this->instantiator = new Instantiator(); + $this->instantiator = Instantiator::withConstructor(); } public function stories(): StoryManager @@ -83,8 +89,6 @@ public function defaultProxyAutoRefresh(): bool } if (null === $this->defaultProxyAutoRefresh) { - trigger_deprecation('zenstruck\foundry', '1.9', 'Not explicitly configuring the default proxy auto-refresh is deprecated and will default to "true" in 2.0. Use "zenstruck_foundry.auto_refresh_proxies" in the bundle config or TestState::enableDefaultProxyAutoRefresh()/disableDefaultProxyAutoRefresh().'); - $this->defaultProxyAutoRefresh = false; } @@ -135,6 +139,17 @@ public function enableDefaultProxyAutoRefresh(): self public function disableDefaultProxyAutoRefresh(): self { + trigger_deprecation( + 'zenstruck\foundry', + '1.38.0', + <<defaultProxyAutoRefresh = false; return $this; @@ -147,6 +162,8 @@ public function isFlushingEnabled(): bool public function disableDatabaseReset(): self { + trigger_deprecation('zenstruck\foundry', '1.38.0', 'Disabling database reset via bundle configuration is deprecated and will be removed in 2.0. Instead you should not use "%s" trait in your test.', ResetDatabase::class); + $this->databaseResetEnabled = false; return $this; @@ -175,16 +192,16 @@ public function delayFlush(callable $callback): mixed /** * @template TObject of object * @phpstan-param Proxy|TObject|class-string $objectOrClass - * @phpstan-return RepositoryProxy + * @phpstan-return ($proxy is true ? ProxyRepositoryDecorator : RepositoryDecorator) */ - public function repositoryFor(object|string $objectOrClass): RepositoryProxy + public function repositoryFor(object|string $objectOrClass, bool $proxy): RepositoryDecorator { if (!$this->isPersistEnabled()) { throw new \RuntimeException('Cannot get repository when persist is disabled.'); } if ($objectOrClass instanceof Proxy) { - $objectOrClass = $objectOrClass->object(); + $objectOrClass = $objectOrClass->_real(); } if (!\is_string($objectOrClass)) { @@ -198,7 +215,7 @@ public function repositoryFor(object|string $objectOrClass): RepositoryProxy throw new \RuntimeException(\sprintf('No repository registered for "%s".', $objectOrClass)); } - return new RepositoryProxy($repository); + return $proxy ? new ProxyRepositoryDecorator($repository) : new RepositoryDecorator($repository); } public function objectManagerFor(object|string $objectOrClass): ObjectManager @@ -249,11 +266,19 @@ public function getOdmObjectManagersToReset(): array public function disablePersist(): void { + if (!self::hasManagerRegistry()) { + trigger_deprecation('zenstruck\foundry', '1.38.0', 'Calling function "disable_persisting()" when Foundry is booted without doctrine is deprecated and will throw an exception in 2.0.'); + } + $this->persistEnabled = false; } public function enablePersist(): void { + if (!self::hasManagerRegistry()) { + trigger_deprecation('zenstruck\foundry', '1.38.0', 'Calling function "enable_persisting()" when Foundry is booted without doctrine is deprecated and will throw an exception in 2.0.'); + } + $this->persistEnabled = true; } diff --git a/src/Factory.php b/src/Factory.php index 2763a42bb..be480df97 100644 --- a/src/Factory.php +++ b/src/Factory.php @@ -19,7 +19,11 @@ use Faker; use Zenstruck\Foundry\Exception\FoundryBootException; use Zenstruck\Foundry\Persistence\InversedRelationshipPostPersistCallback; +use Zenstruck\Foundry\Persistence\PersistentObjectFactory; +use Zenstruck\Foundry\Persistence\PersistentProxyObjectFactory; use Zenstruck\Foundry\Persistence\PostPersistCallback; +use Zenstruck\Foundry\Persistence\Proxy; +use Zenstruck\Foundry\Proxy as ProxyObject; /** * @template TObject of object @@ -44,7 +48,7 @@ class Factory private bool $cascadePersist = false; /** @var array */ - private array $attributeSet = []; + private array $attributes = []; /** @var callable[] */ private array $beforeInstantiate = []; @@ -66,11 +70,11 @@ public function __construct(string $class, array|callable $defaultAttributes = [ } $this->class = $class; - $this->attributeSet[] = $defaultAttributes; + $this->attributes[] = $defaultAttributes; } /** - * @phpstan-return list> + * @phpstan-return ($this is PersistentProxyObjectFactory ? list> : list) */ public function __call(string $name, array $arguments): array { @@ -80,17 +84,29 @@ public function __call(string $name, array $arguments): array trigger_deprecation('zenstruck/foundry', '1.7', 'Calling instance method "%1$s::createMany()" is deprecated and will be removed in 2.0, use e.g. "%1$s::new()->stateAdapter()->many(%2$d)->create()" instead.', static::class, $arguments[0]); - return $this->many($arguments[0])->create($arguments[1] ?? []); + return $this->many($arguments[0])->create($arguments[1] ?? [], noProxy: $this->shouldUseProxy()); } /** - * @return Proxy&TObject - * @phpstan-return Proxy + * @final + * + * @return (Proxy&TObject)|TObject + * @phpstan-return ($noProxy is true ? TObject: Proxy) */ - final public function create(array|callable $attributes = []): Proxy - { + public function create( + array|callable $attributes = [], + /** + * @deprecated + * @internal + */ + bool $noProxy = false, + ): object { + if (2 === \count(\func_get_args()) && !\str_starts_with(\debug_backtrace(options: \DEBUG_BACKTRACE_IGNORE_ARGS, limit: 1)[0]['class'] ?? '', 'Zenstruck\Foundry')) { + trigger_deprecation('zenstruck\foundry', '1.38.0', 'Parameter "$noProxy" of method "%s()" is deprecated and will be removed in Foundry 2.0.', __METHOD__); + } + // merge the factory attribute set with the passed attributes - $attributeSet = \array_merge($this->attributeSet, [$attributes]); + $attributeSet = \array_merge($this->attributes, [$attributes]); // normalize each attribute set and collapse $attributes = \array_merge(...\array_map(fn(callable|array $attributes): array => $this->normalizeAttributes($attributes), $attributeSet)); @@ -139,19 +155,18 @@ final public function create(array|callable $attributes = []): Proxy $callback($object, $attributes); } - $proxy = new Proxy($object); - - if (!$this->isPersisting()) { - return $proxy; + if (!$this->isPersisting(calledInternally: true)) { + return $noProxy ? $object : new ProxyObject($object); } + $proxy = new ProxyObject($object); + if ($this->cascadePersist && !$postPersistCallbacks) { return $proxy; } - return $proxy - ->save() - ->withoutAutoRefresh(function(Proxy $proxy) use ($attributes, $postPersistCallbacks): void { + $proxy->_save() + ->_withoutAutoRefresh(function(ProxyObject $proxy) use ($attributes, $postPersistCallbacks): void { $callbacks = [...$postPersistCallbacks, ...$this->afterPersist]; if (!$callbacks) { @@ -162,20 +177,22 @@ final public function create(array|callable $attributes = []): Proxy $proxy->executeCallback($callback, $attributes); } - $proxy->save(); // save again as afterPersist events may have modified + $proxy->_save(); // save again as afterPersist events may have modified }) ; + + return $noProxy ? $proxy->_real() : $proxy; } /** * @param int|null $max If set, when created, the collection will be a random size between $min and $max * - * @return FactoryCollection + * @return ($this is PersistentProxyObjectFactory ? FactoryCollection> : FactoryCollection) */ final public function many(int $min, ?int $max = null): FactoryCollection { if (!$max) { - return FactoryCollection::set($this, $min); + return FactoryCollection::many($this, $min); } return FactoryCollection::range($this, $min, $max); @@ -184,7 +201,7 @@ final public function many(int $min, ?int $max = null): FactoryCollection /** * @param iterable>|callable(): iterable> $sequence * - * @return FactoryCollection + * @return ($this is PersistentProxyObjectFactory ? FactoryCollection> : FactoryCollection) */ final public function sequence(iterable|callable $sequence): FactoryCollection { @@ -198,8 +215,17 @@ final public function sequence(iterable|callable $sequence): FactoryCollection /** * @return static */ - public function withoutPersisting(): self - { + public function withoutPersisting( + /** + * @internal + * @deprecated + */ + bool $calledInternally = false, + ): self { + if (!$calledInternally && !$this instanceof PersistentObjectFactory) { + trigger_deprecation('zenstruck\foundry', '1.38.0', 'Calling "withoutPersisting()" on a non-persistent factory class is deprecated and will trigger an error in 2.0.', __METHOD__); + } + $cloned = clone $this; $cloned->persist = false; @@ -209,14 +235,23 @@ public function withoutPersisting(): self /** * @param array|callable $attributes * + * @deprecated use with() instead + * * @return static */ final public function withAttributes($attributes = []): self { - $cloned = clone $this; - $cloned->attributeSet[] = $attributes; + trigger_deprecation('zenstruck\foundry', '1.38.0', 'Method "%s()" is deprecated and will be removed in 2.0. Use "%s::with()" instead.', __METHOD__, self::class); - return $cloned; + return $this->with($attributes); + } + + final public function with(array|callable $attributes = []): static + { + $clone = clone $this; + $clone->attributes[] = $attributes; + + return $clone; } /** @@ -252,6 +287,10 @@ final public function afterInstantiate(callable $callback): self */ final public function afterPersist(callable $callback): self { + if (!$this instanceof PersistentObjectFactory) { + trigger_deprecation('zenstruck\foundry', '1.38.0', 'Calling "afterPersist()" on a non-persistent factory class is deprecated and will trigger an error in 2.0.', __METHOD__); + } + $cloned = clone $this; $cloned->afterPersist[] = $callback; @@ -315,6 +354,13 @@ final public static function isBooted(): bool final public static function faker(): Faker\Generator { + if ( + null === ($calledClass = \debug_backtrace(options: \DEBUG_BACKTRACE_IGNORE_ARGS, limit: 2)[1]['class'] ?? null) + || !\is_a($calledClass, self::class, allow_string: true) + ) { + trigger_deprecation('zenstruck\foundry', '1.38.0', 'Method "%s()" will be protected in Foundry 2.0 and should not be called from outside of a factory. Use function "Zenstruck\Foundry\faker()" instead.', __METHOD__); + } + try { return self::configuration()->faker(); } catch (FoundryBootException) { @@ -322,23 +368,51 @@ final public static function faker(): Faker\Generator } } + /** + * @deprecated + */ final public static function delayFlush(callable $callback): mixed { + trigger_deprecation('zenstruck\foundry', '1.38.0', 'Method "%s()" is deprecated and will be removed in Foundry 2.0. Use "Zenstruck\Foundry\Persistence\flush_after()" instead.', __METHOD__); + return self::configuration()->delayFlush($callback); } /** * @internal * - * @phpstan-return class-string + * @return TObject */ - final protected function class(): string + public function createAndUproxify(): object { - return $this->class; + $object = $this->create( + noProxy: !$this->shouldUseProxy(), + ); + + return $object instanceof Proxy ? $object->_real() : $object; } - protected function isPersisting(): bool + /** + * @internal + * + * @return ($this is PersistentProxyObjectFactory ? true : false) + */ + public function shouldUseProxy(): bool { + return $this instanceof PersistentProxyObjectFactory; + } + + protected function isPersisting( + /** + * @internal + * @deprecated + */ + bool $calledInternally = false, + ): bool { + if (!$calledInternally && !$this instanceof PersistentObjectFactory) { + trigger_deprecation('zenstruck\foundry', '1.38.0', 'Calling "isPersisting()" on a non-persistent factory class is deprecated and will trigger an error in 2.0.', __METHOD__); + } + if (!$this->persist || !self::configuration()->isPersistEnabled() || !self::configuration()->hasManagerRegistry()) { return false; } @@ -370,8 +444,8 @@ private function normalizeAttributes(array|callable $attributes): array private function normalizeAttribute(mixed $value, string $name): mixed { - if ($value instanceof Proxy) { - return $value->isPersisted() ? $value->refresh()->object() : $value->object(); + if ($value instanceof ProxyObject) { + return $value->isPersisted(calledInternally: true) ? $value->_refresh()->_real() : $value->_real(); } if ($value instanceof FactoryCollection) { @@ -389,13 +463,13 @@ private function normalizeAttribute(mixed $value, string $name): mixed return \is_object($value) ? self::normalizeObject($value) : $value; } - if (!$this->isPersisting()) { + if (!$this->isPersisting(calledInternally: true)) { // ensure attribute Factories' are also not persisted - $value = $value->withoutPersisting(); + $value = $value->withoutPersisting(calledInternally: true); } if (!self::configuration()->hasManagerRegistry()) { - return $value->create()->object(); + return $value->createAndUproxify(); } try { @@ -403,17 +477,17 @@ private function normalizeAttribute(mixed $value, string $name): mixed if (!$objectManager instanceof EntityManagerInterface || $objectManager->getClassMetadata($value->class)->isEmbeddedClass) { // we may deal with ODM document or ORM\Embedded - return $value->create()->object(); + return $value->createAndUproxify(); } } catch (\Throwable) { // not persisted object - return $value->create()->object(); + return $value->createAndUproxify(); } $relationshipMetadata = self::getRelationshipMetadata($objectManager, $this->class, $name); if (!$relationshipMetadata) { - return $value->create()->object(); + return $value->createAndUproxify(); } if ($relationshipMetadata['isOwningSide']) { @@ -423,7 +497,7 @@ private function normalizeAttribute(mixed $value, string $name): mixed $relationshipField = $relationshipMetadata['inversedField']; $cascadePersist = $relationshipMetadata['cascade']; - if ($this->isPersisting() && null !== $relationshipField && false === $cascadePersist) { + if ($this->isPersisting(calledInternally: true) && null !== $relationshipField && false === $cascadePersist) { return new InversedRelationshipPostPersistCallback($value, $relationshipField, $isCollection); } } @@ -432,13 +506,17 @@ private function normalizeAttribute(mixed $value, string $name): mixed $value = $value->withCascadePersist(); } - return $value->create()->object(); + return $value->createAndUproxify(); } private static function normalizeObject(object $object): object { + if ((new \ReflectionClass($object::class))->isFinal()) { + return $object; + } + try { - return Proxy::createFromPersisted($object)->refresh()->object(); + return ProxyObject::createFromPersisted($object)->_refresh()->_real(); } catch (\RuntimeException) { return $object; } diff --git a/src/FactoryCollection.php b/src/FactoryCollection.php index 6eea33942..3a5fd6240 100644 --- a/src/FactoryCollection.php +++ b/src/FactoryCollection.php @@ -32,7 +32,7 @@ final class FactoryCollection implements \IteratorAggregate * *@deprecated using directly FactoryCollection's constructor is deprecated. It will be private in v2. Use named constructors instead. */ - public function __construct(private Factory $factory, ?int $min = null, ?int $max = null, private ?iterable $sequence = null, bool $calledInternally = false) + public function __construct(public Factory $factory, ?int $min = null, ?int $max = null, private ?iterable $sequence = null, bool $calledInternally = false) { if ($max && $min > $max) { throw new \InvalidArgumentException('Min must be less than max.'); @@ -46,7 +46,17 @@ public function __construct(private Factory $factory, ?int $min = null, ?int $ma $this->max = $max ?? $min; } + /** + * @deprecated Use FactoryCollection::many() instead + */ public static function set(Factory $factory, int $count): self + { + trigger_deprecation('zenstruck/foundry', '1.38.0', 'Method %s() is deprecated and will be removed in 2.0. Use "%s::many()" instead.', __METHOD__, __CLASS__); + + return self::many($factory, $count); + } + + public static function many(Factory $factory, int $count): self { return new self($factory, $count, null, null, true); } @@ -65,16 +75,25 @@ public static function sequence(Factory $factory, iterable $sequence): self } /** - * @return list> - * - * @phpstan-return list> + * @return list */ - public function create(array|callable $attributes = []): array - { + public function create( + array|callable $attributes = [], + /** + * @deprecated + * @internal + */ + bool $noProxy = false, + ): array { + if (2 === \count(\func_get_args()) && !\str_starts_with(\debug_backtrace(options: \DEBUG_BACKTRACE_IGNORE_ARGS, limit: 1)[0]['class'] ?? '', 'Zenstruck\Foundry')) { + trigger_deprecation('zenstruck\foundry', '1.38.0', 'Parameter "$noProxy" of method "%s()" is deprecated and will be removed in Foundry 2.0.', __METHOD__); + } + $objects = []; foreach ($this->all() as $i => $factory) { $objects[] = $factory->create( \is_callable($attributes) ? $attributes($i + 1) : $attributes, + $noProxy || !$factory->shouldUseProxy(), ); } @@ -97,7 +116,7 @@ public function all(): array $factories = []; foreach ($this->sequence as $attributes) { - $factories[] = (clone $this->factory)->withAttributes($attributes); + $factories[] = (clone $this->factory)->with($attributes); } return $factories; @@ -105,6 +124,8 @@ public function all(): array public function factory(): Factory { + trigger_deprecation('zenstruck\foundry', '1.38.0', 'Method "%s()" is deprecated and will be removed in Foundry 2.0. Use public property %s::$factory instead', __METHOD__, __CLASS__); + return $this->factory; } diff --git a/src/Instantiator.php b/src/Instantiator.php index bb33dbed1..323782479 100644 --- a/src/Instantiator.php +++ b/src/Instantiator.php @@ -11,259 +11,25 @@ namespace Zenstruck\Foundry; -use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException; -use Symfony\Component\PropertyAccess\PropertyAccess; -use Symfony\Component\PropertyAccess\PropertyAccessor; - -use function Symfony\Component\String\u; - -/** - * @author Kevin Bond - */ -final class Instantiator -{ - private static ?PropertyAccessor $propertyAccessor = null; - - private bool $withoutConstructor = false; - - private bool $allowExtraAttributes = false; - - private array $extraAttributes = []; - - private bool $alwaysForceProperties = false; - - /** @var string[] */ - private array $forceProperties = []; - - /** - * @param class-string $class - */ - public function __invoke(array $attributes, string $class): object - { - $object = $this->instantiate($class, $attributes); - - foreach ($attributes as $attribute => $value) { - if (0 === \mb_strpos($attribute, 'optional:')) { - trigger_deprecation('zenstruck\foundry', '1.5.0', 'Using "optional:" attribute prefixes is deprecated, use Instantiator::allowExtraAttributes() instead (https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#instantiation).'); - continue; - } - - if (\in_array($attribute, $this->extraAttributes, true)) { - continue; - } - - if ($this->alwaysForceProperties || \in_array($attribute, $this->forceProperties, true)) { - try { - self::forceSet($object, $attribute, $value); - } catch (\InvalidArgumentException $e) { - if (!$this->allowExtraAttributes) { - throw $e; - } - } - - continue; - } - - if (0 === \mb_strpos($attribute, 'force:')) { - trigger_deprecation('zenstruck\foundry', '1.5.0', 'Using "force:" property prefixes is deprecated, use Instantiator::alwaysForceProperties() instead (https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#instantiation).'); - - self::forceSet($object, \mb_substr($attribute, 6), $value); - - continue; - } - - try { - self::propertyAccessor()->setValue($object, $attribute, $value); - } catch (NoSuchPropertyException $e) { - // see if attribute was snake/kebab cased - try { - self::propertyAccessor()->setValue($object, self::camel($attribute), $value); - trigger_deprecation('zenstruck\foundry', '1.5.0', 'Using a differently cased attribute is deprecated, use the same case as the object property instead.'); - } catch (NoSuchPropertyException $e) { - if (!$this->allowExtraAttributes) { - throw new \InvalidArgumentException(\sprintf('Cannot set attribute "%s" for object "%s" (not public and no setter).', $attribute, $class), 0, $e); - } - } - } - } - - return $object; - } - - /** - * Instantiate objects without calling the constructor. - */ - public function withoutConstructor(): self - { - $this->withoutConstructor = true; - - return $this; - } - - /** - * Ignore attributes that can't be set to object. - * - * @param string[] $attributes The attributes you'd like the instantiator to ignore (if empty, ignore any extra) - */ - public function allowExtraAttributes(array $attributes = []): self - { - if (empty($attributes)) { - $this->allowExtraAttributes = true; - } - - $this->extraAttributes = $attributes; - - return $this; - } - - /** - * Always force properties, never use setters (still uses constructor unless disabled). - * - * @param string[] $properties The properties you'd like the instantiator to "force set" (if empty, force set all) - */ - public function alwaysForceProperties(array $properties = []): self - { - if (empty($properties)) { - $this->alwaysForceProperties = true; - } - - $this->forceProperties = $properties; - - return $this; - } - - /** - * @throws \InvalidArgumentException if property does not exist for $object - */ - public static function forceSet(object $object, string $property, mixed $value): void - { - self::accessibleProperty($object, $property)->setValue($object, $value); - } - - /** - * @return mixed - */ - public static function forceGet(object $object, string $property) - { - return self::accessibleProperty($object, $property)->getValue($object); - } - - private static function propertyAccessor(): PropertyAccessor - { - return self::$propertyAccessor ?: self::$propertyAccessor = PropertyAccess::createPropertyAccessor(); - } - - private static function accessibleProperty(object $object, string $name): \ReflectionProperty - { - $class = new \ReflectionClass($object); - - // try fetching first by exact name, if not found, try camel-case - $property = self::reflectionProperty($class, $name); - - if (!$property && $property = self::reflectionProperty($class, self::camel($name))) { - trigger_deprecation('zenstruck\foundry', '1.5.0', 'Using a differently cased attribute is deprecated, use the same case as the object property instead.'); - } - - if (!$property) { - throw new \InvalidArgumentException(\sprintf('Class "%s" does not have property "%s".', $class->getName(), $name)); - } - - if (!$property->isPublic()) { - $property->setAccessible(true); - } - - return $property; - } - - private static function reflectionProperty(\ReflectionClass $class, string $name): ?\ReflectionProperty - { - try { - return $class->getProperty($name); - } catch (\ReflectionException) { - if ($class = $class->getParentClass()) { - return self::reflectionProperty($class, $name); - } - } - - return null; - } - - /** - * Check if parameter value was passed as the exact name - if not, try snake-cased, then kebab-cased versions. - */ - private static function attributeNameForParameter(\ReflectionParameter $parameter, array $attributes): ?string - { - // try exact - $name = $parameter->getName(); - - if (\array_key_exists($name, $attributes)) { - return $name; - } - - // try snake case - $name = self::snake($name); - - if (\array_key_exists($name, $attributes)) { - trigger_deprecation('zenstruck\foundry', '1.5.0', 'Using a differently cased attribute is deprecated, use the same case as the object property instead.'); - - return $name; - } - - // try kebab case - $name = \str_replace('_', '-', $name); - - if (\array_key_exists($name, $attributes)) { - trigger_deprecation('zenstruck\foundry', '1.5.0', 'Using a differently cased attribute is deprecated, use the same case as the object property instead.'); - - return $name; - } - - return null; - } - - private static function camel(string $string): string - { - return u($string)->camel(); - } +use Zenstruck\Foundry\Object\Instantiator as NewInstatiator; + +if (!\class_exists(NewInstatiator::class, false)) { + trigger_deprecation( + 'zenstruck\foundry', + '1.38.0', + 'Class "%s" is deprecated and will be removed in version 2.0. Use "%s" instead.', + Instantiator::class, + NewInstatiator::class, + ); +} - private static function snake(string $string): string - { - return u($string)->snake(); - } +\class_alias(NewInstatiator::class, Instantiator::class); +if (false) { /** - * @param class-string $class + * @deprecated */ - private function instantiate(string $class, array &$attributes): object + final class Instantiator extends NewInstatiator { - $class = new \ReflectionClass($class); - $constructor = $class->getConstructor(); - - if ($this->withoutConstructor || !$constructor || !$constructor->isPublic()) { - return $class->newInstanceWithoutConstructor(); - } - - $arguments = []; - - foreach ($constructor->getParameters() as $parameter) { - $name = self::attributeNameForParameter($parameter, $attributes); - - if ($name && \array_key_exists($name, $attributes)) { - if ($parameter->isVariadic()) { - $arguments = \array_merge($arguments, $attributes[$name]); - } else { - $arguments[] = $attributes[$name]; - } - } elseif ($parameter->isDefaultValueAvailable()) { - $arguments[] = $parameter->getDefaultValue(); - } else { - throw new \InvalidArgumentException(\sprintf('Missing constructor argument "%s" for "%s".', $parameter->getName(), $class->getName())); - } - - // unset attribute so it isn't used when setting object properties - unset($attributes[$name]); - } - - return $class->newInstance(...$arguments); } } diff --git a/src/ModelFactory.php b/src/ModelFactory.php index b40aa28bc..625920885 100644 --- a/src/ModelFactory.php +++ b/src/ModelFactory.php @@ -11,305 +11,72 @@ namespace Zenstruck\Foundry; -use Zenstruck\Foundry\Exception\FoundryBootException; +use Zenstruck\Foundry\Persistence\PersistentObjectFactory; +use Zenstruck\Foundry\Persistence\PersistentProxyObjectFactory; +use Zenstruck\Foundry\Persistence\Proxy; /** * @template TModel of object - * @template-extends Factory + * @template-extends PersistentProxyObjectFactory * * @method static Proxy[]|TModel[] createMany(int $number, array|callable $attributes = []) + * + * @phpstan-method FactoryCollection> sequence(iterable>|callable(): iterable> $sequence) + * @phpstan-method FactoryCollection> many(int $min, int|null $max = null) + * + * @phpstan-method static list> createSequence(iterable>|callable(): iterable> $sequence) * @phpstan-method static list> createMany(int $number, array|callable $attributes = []) * * @author Kevin Bond + * + * @deprecated use PersistentProxyObjectFactory instead */ -abstract class ModelFactory extends Factory +abstract class ModelFactory extends PersistentProxyObjectFactory { public function __construct() { - parent::__construct(static::getClass()); - } - - /** - * @phpstan-return list> - */ - public static function __callStatic(string $name, array $arguments): array - { - if ('createMany' !== $name) { - throw new \BadMethodCallException(\sprintf('Call to undefined static method "%s::%s".', static::class, $name)); - } - - return static::new()->many($arguments[0])->create($arguments[1] ?? []); - } - - /** - * @param array|callable|string $defaultAttributes If string, assumes state - * @param string ...$states Optionally pass default states (these must be methods on your ObjectFactory with no arguments) - */ - final public static function new(array|callable|string $defaultAttributes = [], string ...$states): static - { - if (\is_string($defaultAttributes)) { - $states = \array_merge([$defaultAttributes], $states); - $defaultAttributes = []; - } - - try { - $factory = self::isBooted() ? self::configuration()->factories()->create(static::class) : new static(); - } catch (\ArgumentCountError $e) { - throw new \RuntimeException('Model Factories with dependencies (Model Factory services) cannot be created before foundry is booted.', 0, $e); - } - - $factory = $factory - ->withAttributes(static fn(): array => $factory->getDefaults()) - ->withAttributes($defaultAttributes); - - try { - if (!Factory::configuration()->isPersistEnabled()) { - $factory = $factory->withoutPersisting(); - } - } catch (FoundryBootException) { - } - - $factory = $factory->initialize(); - - if (!$factory instanceof static) { - throw new \TypeError(\sprintf('"%1$s::initialize()" must return an instance of "%1$s".', static::class)); - } - - foreach ($states as $state) { - $factory = $factory->{$state}(); - } - - return $factory; - } - - /** - * A shortcut to create a single model without states. - * - * @return Proxy&TModel - * @phpstan-return Proxy - */ - final public static function createOne(array $attributes = []): Proxy - { - return static::new()->create($attributes); - } - - /** - * A shortcut to create multiple models, based on a sequence, without states. - * - * @param iterable>|callable(): iterable> $sequence - * - * @return list> - * @phpstan-return list> - */ - final public static function createSequence(iterable|callable $sequence): array - { - return static::new()->sequence($sequence)->create(); - } - - /** - * Try and find existing object for the given $attributes. If not found, - * instantiate and persist. - * - * @return Proxy&TModel - * @phpstan-return Proxy - */ - final public static function findOrCreate(array $attributes): Proxy - { - try { - if ($found = static::repository()->find($attributes)) { - return $found; - } - } catch (FoundryBootException) { - } - - return static::new()->create($attributes); - } - - /** - * @see RepositoryProxy::first() - * - * @return Proxy&TModel - * @phpstan-return Proxy - * - * @throws \RuntimeException If no entities exist - */ - final public static function first(string $sortedField = 'id'): Proxy - { - if (null === $proxy = static::repository()->first($sortedField)) { - throw new \RuntimeException(\sprintf('No "%s" objects persisted.', static::getClass())); - } - - return $proxy; - } - - /** - * @see RepositoryProxy::last() - * - * @return Proxy&TModel - * @phpstan-return Proxy - * - * @throws \RuntimeException If no entities exist - */ - final public static function last(string $sortedField = 'id'): Proxy - { - if (null === $proxy = static::repository()->last($sortedField)) { - throw new \RuntimeException(\sprintf('No "%s" objects persisted.', static::getClass())); - } - - return $proxy; - } - - /** - * @see RepositoryProxy::random() - * - * @return Proxy&TModel - * @phpstan-return Proxy - */ - final public static function random(array $attributes = []): Proxy - { - return static::repository()->random($attributes); - } - - /** - * Fetch one random object and create a new object if none exists. - * - * @return Proxy&TModel - * @phpstan-return Proxy - */ - final public static function randomOrCreate(array $attributes = []): Proxy - { - try { - return static::repository()->random($attributes); - } catch (\RuntimeException) { - return static::new()->create($attributes); - } - } + $newFactoryClass = (new \ReflectionClass(static::class()))->isFinal() ? PersistentObjectFactory::class : ObjectFactory::class; - /** - * @see RepositoryProxy::randomSet() - * - * @return list> - * @phpstan-return list> - */ - final public static function randomSet(int $number, array $attributes = []): array - { - return static::repository()->randomSet($number, $attributes); - } + trigger_deprecation( + 'zenstruck\foundry', '1.38.0', + <<> - * @phpstan-return list> - */ - final public static function randomRange(int $min, int $max, array $attributes = []): array - { - return static::repository()->randomRange($min, $max, $attributes); + parent::__construct(); } - /** - * @see RepositoryProxy::count() - */ - final public static function count(array $criteria = []): int + public static function class(): string { - return static::repository()->count($criteria); - } - - /** - * @see RepositoryProxy::truncate() - */ - final public static function truncate(): void - { - static::repository()->truncate(); - } - - /** - * @see RepositoryProxy::findAll() - * - * @return list> - * @phpstan-return list> - */ - final public static function all(): array - { - return static::repository()->findAll(); - } - - /** - * @see RepositoryProxy::find() - * - * @phpstan-param Proxy|array|mixed $criteria - * @phpstan-return Proxy - * - * @return Proxy&TModel - * - * @throws \RuntimeException If no entity found - */ - final public static function find($criteria): Proxy - { - if (null === $proxy = static::repository()->find($criteria)) { - throw new \RuntimeException(\sprintf('Could not find "%s" object.', static::getClass())); - } - - return $proxy; - } - - /** - * @see RepositoryProxy::findBy() - * - * @return list> - * @phpstan-return list> - */ - final public static function findBy(array $attributes): array - { - return static::repository()->findBy($attributes); - } - - final public static function assert(): RepositoryAssertions - { - try { - return static::repository()->assert(); - } catch (\Throwable $e) { - throw new \RuntimeException(\sprintf('Cannot create repository assertion: %s', $e->getMessage()), previous: $e); - } - } - - /** - * @phpstan-return RepositoryProxy - */ - final public static function repository(): RepositoryProxy - { - return static::configuration()->repositoryFor(static::getClass()); + return static::getClass(); } /** - * @internal * @phpstan-return class-string + * + * @deprecated use class() instead */ - final public static function getEntityClass(): string - { - return static::getClass(); - } - - /** @phpstan-return class-string */ abstract protected static function getClass(): string; /** - * Override to add default instantiator and default afterInstantiate/afterPersist events. + * @return mixed[] * - * @return static + * @deprecated use defaults() instead */ - protected function initialize() - { - return $this; - } + abstract protected function getDefaults(): array; - final protected function addState(array|callable $attributes = []): static + protected function defaults(): array|callable { - return $this->withAttributes($attributes); + return $this->getDefaults(); } - - /** - * @return mixed[] - */ - abstract protected function getDefaults(): array; } diff --git a/src/ModelFactoryManager.php b/src/ModelFactoryManager.php index 0d096ce43..ed9cd579a 100644 --- a/src/ModelFactoryManager.php +++ b/src/ModelFactoryManager.php @@ -19,16 +19,16 @@ final class ModelFactoryManager { /** - * @param ModelFactory[] $factories + * @param ObjectFactory[] $factories */ public function __construct(private iterable $factories) { } /** - * @param class-string $class + * @param class-string $class */ - public function create(string $class): ModelFactory + public function create(string $class): ObjectFactory { foreach ($this->factories as $factory) { if ($class === $factory::class) { diff --git a/src/Object/Instantiator.php b/src/Object/Instantiator.php new file mode 100644 index 000000000..ca52a0575 --- /dev/null +++ b/src/Object/Instantiator.php @@ -0,0 +1,360 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Object; + +use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException; +use Symfony\Component\PropertyAccess\PropertyAccess; +use Symfony\Component\PropertyAccess\PropertyAccessor; + +use function Symfony\Component\String\u; + +/** + * @author Kevin Bond + * + * @final + * + * @method static self withoutConstructor() + */ +class Instantiator +{ + private static ?PropertyAccessor $propertyAccessor = null; + + private bool $useConstructor = true; + + private bool $allowExtraAttributes = false; + + private array $extraAttributes = []; + + private bool $alwaysForceProperties = false; + + /** @var string[] */ + private array $forceProperties = []; + + public function __construct(bool $calledInternally = false) + { + if (!$calledInternally) { + trigger_deprecation('zenstruck\foundry', '1.38.0', '%1$s constructor will be private in Foundry 2.0. Use either "%1$s::withConstructor()" or "%1$s::withoutConstructor()"', self::class); + } + } + + /** + * @param class-string $class + */ + public function __invoke(array $attributes, string $class): object + { + $object = $this->instantiate($class, $attributes); + + foreach ($attributes as $attribute => $value) { + if (0 === \mb_strpos($attribute, 'optional:')) { + trigger_deprecation('zenstruck\foundry', '1.5.0', 'Using "optional:" attribute prefixes is deprecated, use Instantiator::allowExtra() instead (https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#instantiation).'); + continue; + } + + if (\in_array($attribute, $this->extraAttributes, true)) { + continue; + } + + if ($this->alwaysForceProperties || \in_array($attribute, $this->forceProperties, true)) { + try { + self::forceSet($object, $attribute, $value, calledInternally: true); + } catch (\InvalidArgumentException $e) { + if (!$this->allowExtraAttributes) { + throw $e; + } + } + + continue; + } + + if (0 === \mb_strpos($attribute, 'force:')) { + trigger_deprecation('zenstruck\foundry', '1.5.0', 'Using "force:" property prefixes is deprecated, use Instantiator::alwaysForce() instead (https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#instantiation).'); + + self::forceSet($object, \mb_substr($attribute, 6), $value, calledInternally: true); + + continue; + } + + try { + self::propertyAccessor()->setValue($object, $attribute, $value); + } catch (NoSuchPropertyException $e) { + // see if attribute was snake/kebab cased + try { + self::propertyAccessor()->setValue($object, self::camel($attribute), $value); + trigger_deprecation('zenstruck\foundry', '1.5.0', 'Using a differently cased attribute is deprecated, use the same case as the object property instead.'); + } catch (NoSuchPropertyException $e) { + if (!$this->allowExtraAttributes) { + throw new \InvalidArgumentException(\sprintf('Cannot set attribute "%s" for object "%s" (not public and no setter).', $attribute, $class), 0, $e); + } + } + } + } + + return $object; + } + + public function __call(string $name, array $arguments): self + { + if ('withoutConstructor' !== $name) { + throw new \BadMethodCallException(\sprintf('Call to undefined method "%s::%s".', static::class, $name)); + } + + trigger_deprecation('zenstruck/foundry', '1.38.0', 'Calling instance method "%1$s::withoutConstructor()" is deprecated and will be removed in 2.0. Use static call instead: "%1$s::withoutConstructor()" instead.', static::class); + + $this->useConstructor = false; + + return $this; + } + + public static function __callStatic(string $name, array $arguments): self + { + if ('withoutConstructor' !== $name) { + throw new \BadMethodCallException(\sprintf('Call to undefined method "%s::%s".', static::class, $name)); + } + + $instance = new self(calledInternally: true); + + $instance->useConstructor = false; + + return $instance; + } + + public static function withConstructor(): self + { + return new self(calledInternally: true); + } + + /** + * Ignore attributes that can't be set to object. + * + * @param string[] $attributes The attributes you'd like the instantiator to ignore (if empty, ignore any extra) + * + * @deprecated Use self::allowExtra() instead + */ + public function allowExtraAttributes(array $attributes = []): self + { + trigger_deprecation('zenstruck/foundry', '1.38.0', 'Method "Instantiator::allowExtraAttributes()" is deprecated. Please use "Instantiator::allowExtra()" instead.'); + + return $this->allowExtra(...$attributes); + } + + /** + * Ignore attributes that can't be set to object. + * + * @param string $parameters The attributes you'd like the instantiator to ignore (if empty, ignore any extra) + */ + public function allowExtra(string ...$parameters): self + { + if (empty($parameters)) { + $this->allowExtraAttributes = true; + } + + $this->extraAttributes = $parameters; + + return $this; + } + + /** + * Always force properties, never use setters (still uses constructor unless disabled). + * + * @param string[] $properties The properties you'd like the instantiator to "force set" (if empty, force set all) + * + * @deprecated Use self::alwaysForce() instead + */ + public function alwaysForceProperties(array $properties = []): self + { + trigger_deprecation('zenstruck/foundry', '1.38.0', 'Method "Instantiator::alwaysForceProperties()" is deprecated. Please use "Instantiator::alwaysForce()" instead.'); + + return $this->alwaysForce(...$properties); + } + + /** + * Always force properties, never use setters (still uses constructor unless disabled). + * + * @param string $properties The properties you'd like the instantiator to "force set" (if empty, force set all) + */ + public function alwaysForce(string ...$properties): self + { + if (empty($properties)) { + $this->alwaysForceProperties = true; + } + + $this->forceProperties = $properties; + + return $this; + } + + /** + * @deprecated + * @throws \InvalidArgumentException if property does not exist for $object + */ + public static function forceSet( + object $object, + string $property, + mixed $value, + /** + * @internal + */ + bool $calledInternally = false): void + { + if (!$calledInternally) { + trigger_deprecation('zenstruck\foundry', '1.38.0', 'Method "%s()" is deprecated with no replacement.', __METHOD__); + } + + self::accessibleProperty($object, $property)->setValue($object, $value); + } + + /** + * @deprecated + * @return mixed + */ + public static function forceGet( + object $object, + string $property, + /** + * @internal + */ + bool $calledInternally = false, + ) { + if (!$calledInternally) { + trigger_deprecation('zenstruck\foundry', '1.38.0', 'Method "%s()" is deprecated with no replacement.', __METHOD__); + } + + return self::accessibleProperty($object, $property)->getValue($object); + } + + private static function propertyAccessor(): PropertyAccessor + { + return self::$propertyAccessor ?: self::$propertyAccessor = PropertyAccess::createPropertyAccessor(); + } + + private static function accessibleProperty(object $object, string $name): \ReflectionProperty + { + $class = new \ReflectionClass($object); + + // try fetching first by exact name, if not found, try camel-case + $property = self::reflectionProperty($class, $name); + + if (!$property && $property = self::reflectionProperty($class, self::camel($name))) { + trigger_deprecation('zenstruck\foundry', '1.5.0', 'Using a differently cased attribute is deprecated, use the same case as the object property instead.'); + } + + if (!$property) { + throw new \InvalidArgumentException(\sprintf('Class "%s" does not have property "%s".', $class->getName(), $name)); + } + + if (!$property->isPublic()) { + $property->setAccessible(true); + } + + return $property; + } + + private static function reflectionProperty(\ReflectionClass $class, string $name): ?\ReflectionProperty + { + try { + return $class->getProperty($name); + } catch (\ReflectionException) { + if ($class = $class->getParentClass()) { + return self::reflectionProperty($class, $name); + } + } + + return null; + } + + /** + * Check if parameter value was passed as the exact name - if not, try snake-cased, then kebab-cased versions. + */ + private static function attributeNameForParameter(\ReflectionParameter $parameter, array $attributes): ?string + { + // try exact + $name = $parameter->getName(); + + if (\array_key_exists($name, $attributes)) { + return $name; + } + + // try snake case + $name = self::snake($name); + + if (\array_key_exists($name, $attributes)) { + trigger_deprecation('zenstruck\foundry', '1.5.0', 'Using a differently cased attribute is deprecated, use the same case as the object property instead.'); + + return $name; + } + + // try kebab case + $name = \str_replace('_', '-', $name); + + if (\array_key_exists($name, $attributes)) { + trigger_deprecation('zenstruck\foundry', '1.5.0', 'Using a differently cased attribute is deprecated, use the same case as the object property instead.'); + + return $name; + } + + return null; + } + + private static function camel(string $string): string + { + return u($string)->camel()->toString(); + } + + private static function snake(string $string): string + { + return u($string)->snake()->toString(); + } + + /** + * @param class-string $class + */ + private function instantiate(string $class, array &$attributes): object + { + $class = new \ReflectionClass($class); + $constructor = $class->getConstructor(); + + if (!$this->useConstructor || !$constructor || !$constructor->isPublic()) { + if ($this->useConstructor && $constructor && !$constructor->isPublic()) { + trigger_deprecation('zenstruck\foundry', '1.38.0', 'Instantiator was created to instantiate "%s" by calling the constructor whereas the constructor is not public. This is deprecated and will throw an exception in Foundry 2.0. Use "%s::withoutConstructor()" instead or make constructor public.', $class->getName(), self::class); + } + + return $class->newInstanceWithoutConstructor(); + } + + $arguments = []; + + foreach ($constructor->getParameters() as $parameter) { + $name = self::attributeNameForParameter($parameter, $attributes); + + if ($name && \array_key_exists($name, $attributes)) { + if ($parameter->isVariadic()) { + $arguments = \array_merge($arguments, $attributes[$name]); + } else { + $arguments[] = $attributes[$name]; + } + } elseif ($parameter->isDefaultValueAvailable()) { + $arguments[] = $parameter->getDefaultValue(); + } else { + throw new \InvalidArgumentException(\sprintf('Missing constructor argument "%s" for "%s".', $parameter->getName(), $class->getName())); + } + + // unset attribute so it isn't used when setting object properties + unset($attributes[$name]); + } + + return $class->newInstance(...$arguments); + } +} + +\class_exists(\Zenstruck\Foundry\Instantiator::class); diff --git a/src/ObjectFactory.php b/src/ObjectFactory.php new file mode 100644 index 000000000..992d04e1d --- /dev/null +++ b/src/ObjectFactory.php @@ -0,0 +1,168 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry; + +use Zenstruck\Foundry\Exception\FoundryBootException; +use Zenstruck\Foundry\Persistence\PersistentObjectFactory; + +/** + * @template TModel of object + * @template-extends Factory + * + * @method static TModel[] createMany(int $number, array|callable $attributes = []) + * @phpstan-method static list createMany(int $number, array|callable $attributes = []) + * + * @author Kevin Bond + */ +abstract class ObjectFactory extends Factory +{ + public function __construct() + { + parent::__construct(static::class()); + } + + /** + * @phpstan-return list + */ + public static function __callStatic(string $name, array $arguments): array + { + if ('createMany' !== $name) { + throw new \BadMethodCallException(\sprintf('Call to undefined static method "%s::%s".', static::class, $name)); + } + + return static::new()->many($arguments[0])->create($arguments[1] ?? [], noProxy: true); + } + + /** + * @final + * + * @param array|callable|string $defaultAttributes If string, assumes state + * @param string ...$states Optionally pass default states (these must be methods on your ObjectFactory with no arguments) + */ + public static function new(array|callable|string $defaultAttributes = [], string ...$states): static + { + if (\is_string($defaultAttributes) || \count($states)) { + trigger_deprecation('zenstruck\foundry', '1.38.0', 'Passing states as strings to "Factory::new()" is deprecated and will throw an exception in Foundry 2.0.'); + } + + if (\is_string($defaultAttributes)) { + $states = \array_merge([$defaultAttributes], $states); + $defaultAttributes = []; + } + + try { + $factory = self::isBooted() ? self::configuration()->factories()->create(static::class) : new static(); + } catch (\ArgumentCountError $e) { + throw new \RuntimeException('Model Factories with dependencies (Model Factory services) cannot be created before foundry is booted.', 0, $e); + } + + $factory = $factory + ->with(static fn(): array|callable => $factory->defaults()) + ->with($defaultAttributes); + + try { + if (!\is_a(static::class, PersistentObjectFactory::class, true) || !Factory::configuration()->isPersistEnabled()) { + $factory = $factory->withoutPersisting(calledInternally: true); + } + } catch (FoundryBootException) { + } + + $factory = $factory->initialize(); + + if (!$factory instanceof static) { + throw new \TypeError(\sprintf('"%1$s::initialize()" must return an instance of "%1$s".', static::class)); + } + + foreach ($states as $state) { + $factory = $factory->{$state}(); + } + + return $factory; + } + + /** + * @final + * + * @return TModel + */ + public function create( + array|callable $attributes = [], + /** + * @deprecated + * @internal + */ + bool $noProxy = false, + ): object { + if (2 === \count(\func_get_args()) && !\str_starts_with(\debug_backtrace(options: \DEBUG_BACKTRACE_IGNORE_ARGS, limit: 1)[0]['class'] ?? '', 'Zenstruck\Foundry')) { + trigger_deprecation('zenstruck\foundry', '1.38.0', \sprintf('Parameter "$noProxy" of method "%s()" is deprecated and will be removed in Foundry 2.0.', __METHOD__)); + } + + return parent::create( + $attributes, + noProxy: true, + ); + } + + /** + * @final + * + * A shortcut to create a single model without states. + * + * @return TModel + */ + public static function createOne(array $attributes = []): object + { + return static::new()->create($attributes, noProxy: true); + } + + /** + * @final + * + * A shortcut to create multiple models, based on a sequence, without states. + * + * @param iterable>|callable(): iterable> $sequence + * + * @return list + */ + public static function createSequence(iterable|callable $sequence): array + { + return static::new()->sequence($sequence)->create(noProxy: true); + } + + /** @phpstan-return class-string */ + abstract public static function class(): string; + + /** + * Override to add default instantiator and default afterInstantiate/afterPersist events. + * + * @return static + */ + #[\ReturnTypeWillChange] + protected function initialize() + { + return $this; + } + + /** + * @deprecated use with() instead + */ + final protected function addState(array|callable $attributes = []): static + { + trigger_deprecation('zenstruck\foundry', '1.38.0', \sprintf('Method "%s()" is deprecated and will be removed in version 2.0. Use "%s::with()" instead.', __METHOD__, Factory::class)); + + return $this->with($attributes); + } + + abstract protected function defaults(): array|callable; +} diff --git a/src/Persistence/InversedRelationshipPostPersistCallback.php b/src/Persistence/InversedRelationshipPostPersistCallback.php index eac34ce63..032c347ff 100644 --- a/src/Persistence/InversedRelationshipPostPersistCallback.php +++ b/src/Persistence/InversedRelationshipPostPersistCallback.php @@ -14,7 +14,6 @@ namespace Zenstruck\Foundry\Persistence; use Zenstruck\Foundry\Factory; -use Zenstruck\Foundry\Proxy; /** * @internal @@ -34,6 +33,6 @@ public function __invoke(Proxy $proxy): void $this->relationshipField => $this->isCollection ? [$proxy] : $proxy, ]); - $proxy->refresh(); + $proxy->_refresh(); } } diff --git a/src/Persistence/PersistentObjectFactory.php b/src/Persistence/PersistentObjectFactory.php new file mode 100644 index 000000000..48db346e7 --- /dev/null +++ b/src/Persistence/PersistentObjectFactory.php @@ -0,0 +1,204 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Persistence; + +use Zenstruck\Foundry\Exception\FoundryBootException; +use Zenstruck\Foundry\ObjectFactory; + +/** + * @template TModel of object + * @template-extends ObjectFactory + * + * @method static TModel[] createMany(int $number, array|callable $attributes = []) + * @phpstan-method static list createMany(int $number, array|callable $attributes = []) + * + * @author Kevin Bond + */ +abstract class PersistentObjectFactory extends ObjectFactory +{ + /** + * @final + * + * Try and find existing object for the given $attributes. If not found, + * instantiate and persist. + * + * @return TModel + */ + public static function findOrCreate(array $attributes): object + { + try { + if ($found = static::repository()->find($attributes)) { + return $found; + } + } catch (FoundryBootException) { + } + + return static::new()->create($attributes, noProxy: true); + } + + /** + * @final + * + * @see RepositoryDecorator::first() + * + * @return TModel + * + * @throws \RuntimeException If no entities exist + */ + public static function first(string $sortedField = 'id'): object + { + return static::repository()->first($sortedField) ?? throw new \RuntimeException(\sprintf('No "%s" objects persisted.', static::class())); + } + + /** + * @final + * + * @see RepositoryDecorator::last() + * + * @return TModel + * + * @throws \RuntimeException If no entities exist + */ + public static function last(string $sortedField = 'id'): object + { + return static::repository()->last($sortedField) ?? throw new \RuntimeException(\sprintf('No "%s" objects persisted.', static::class())); + } + + /** + * @final + * + * @see RepositoryDecorator::random() + * + * @return TModel + */ + public static function random(array $attributes = []): object + { + return static::repository()->random($attributes); + } + + /** + * @final + * + * Fetch one random object and create a new object if none exists. + * + * @return TModel + */ + public static function randomOrCreate(array $attributes = []): object + { + try { + return static::repository()->random($attributes); + } catch (\RuntimeException) { + return static::new()->create($attributes, noProxy: true); + } + } + + /** + * @final + * + * @see RepositoryDecorator::randomSet() + * + * @return list + */ + public static function randomSet(int $number, array $attributes = []): array + { + return static::repository()->randomSet($number, $attributes); + } + + /** + * @final + * + * @see RepositoryDecorator::randomRange() + * + * @return list + */ + public static function randomRange(int $min, int $max, array $attributes = []): array + { + return static::repository()->randomRange($min, $max, $attributes); + } + + /** + * @see RepositoryDecorator::count() + */ + final public static function count(array $criteria = []): int + { + return static::repository()->count($criteria); + } + + /** + * @see RepositoryDecorator::truncate() + */ + final public static function truncate(): void + { + static::repository()->truncate(); + } + + /** + * @final + * + * @see RepositoryDecorator::findAll() + * + * @return list + */ + public static function all(): array + { + return static::repository()->findAll(); + } + + /** + * @final + * + * @see RepositoryDecorator::find() + * + * @phpstan-param TModel|array|mixed $criteria + * + * @return TModel + * + * @throws \RuntimeException If no entity found + */ + public static function find($criteria): object + { + return static::repository()->find($criteria) ?? throw new \RuntimeException(\sprintf('Could not find "%s" object.', static::class())); + } + + /** + * @final + * + * @see RepositoryDecorator::findBy() + * + * @return list + */ + public static function findBy(array $attributes): array + { + return static::repository()->findBy($attributes); + } + + final public static function assert(): RepositoryAssertions + { + try { + return static::repository()->assert(); + } catch (\Throwable $e) { + throw new \RuntimeException(\sprintf('Cannot create repository assertion: %s', $e->getMessage()), previous: $e); + } + } + + /** + * @phpstan-return RepositoryDecorator + * + * @final + */ + public static function repository(): RepositoryDecorator + { + return static::configuration()->repositoryFor(static::class(), proxy: false); + } +} diff --git a/src/Persistence/PersistentProxyObjectFactory.php b/src/Persistence/PersistentProxyObjectFactory.php new file mode 100644 index 000000000..3284601be --- /dev/null +++ b/src/Persistence/PersistentProxyObjectFactory.php @@ -0,0 +1,253 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Persistence; + +use Zenstruck\Foundry\Exception\FoundryBootException; +use Zenstruck\Foundry\Factory; +use Zenstruck\Foundry\FactoryCollection; // keep me! + +/** + * @template TModel of object + * @template-extends PersistentObjectFactory + * + * @method static Proxy[]|TModel[] createMany(int $number, array|callable $attributes = []) + * + * @phpstan-method FactoryCollection> sequence(iterable>|callable(): iterable> $sequence) + * @phpstan-method FactoryCollection> many(int $min, int|null $max = null) + * + * @phpstan-method static list> createSequence(iterable>|callable(): iterable> $sequence) + * @phpstan-method static list> createMany(int $number, array|callable $attributes = []) + * + * @author Kevin Bond + */ +abstract class PersistentProxyObjectFactory extends PersistentObjectFactory +{ + /** + * @phpstan-return list> + */ + public static function __callStatic(string $name, array $arguments): array + { + if ('createMany' !== $name) { + throw new \BadMethodCallException(\sprintf('Call to undefined static method "%s::%s".', static::class, $name)); + } + + return static::new()->many($arguments[0])->create($arguments[1] ?? [], noProxy: false); + } + + final public static function new(array|callable|string $defaultAttributes = [], string ...$states): static + { + if ((new \ReflectionClass(static::class()))->isFinal()) { + trigger_deprecation( + 'zenstruck\foundry', '1.38.0', + 'Using a proxy factory with a final class is deprecated and will throw an error in Foundry 2.0. Use "Zenstruck\Foundry\ObjectFactory" instead (don\'t forget to remove all ->object() calls!).', + self::class, + static::class(), + ); + } + + return parent::new($defaultAttributes, ...$states); + } + + /** + * @return Proxy + */ + final public function create( + array|callable $attributes = [], + /** + * @deprecated + * @internal + */ + bool $noProxy = false, + ): object { + if (2 === \count(\func_get_args()) && !\str_starts_with(\debug_backtrace(options: \DEBUG_BACKTRACE_IGNORE_ARGS, limit: 1)[0]['class'] ?? '', 'Zenstruck\Foundry')) { + trigger_deprecation('zenstruck\foundry', '1.38.0', 'Parameter "$noProxy" of method "%s()" is deprecated and will be removed in Foundry 2.0.', __METHOD__); + } + + return Factory::create($attributes, noProxy: false); + } + + /** + * A shortcut to create a single model without states. + * + * @return Proxy&TModel + * @phpstan-return Proxy + */ + final public static function createOne(array $attributes = []): Proxy + { + return static::new()->create($attributes); + } + + /** + * A shortcut to create multiple models, based on a sequence, without states. + * + * @param iterable>|callable(): iterable> $sequence + * + * @return list> + * @phpstan-return list> + */ + final public static function createSequence(iterable|callable $sequence): array + { + return static::new()->sequence($sequence)->create(); + } + + /** + * Try and find existing object for the given $attributes. If not found, + * instantiate and persist. + * + * @return Proxy&TModel + * @phpstan-return Proxy + */ + final public static function findOrCreate(array $attributes): Proxy + { + try { + if ($found = static::repository()->find($attributes)) { + return $found; + } + } catch (FoundryBootException) { + } + + return static::new()->create($attributes); + } + + /** + * @see ProxyRepositoryDecorator::first() + * + * @return Proxy&TModel + * @phpstan-return Proxy + * + * @throws \RuntimeException If no entities exist + */ + final public static function first(string $sortedField = 'id'): Proxy + { + if (null === $proxy = static::repository()->first($sortedField)) { + throw new \RuntimeException(\sprintf('No "%s" objects persisted.', static::class())); + } + + return $proxy; + } + + /** + * @see ProxyRepositoryDecorator::last() + * + * @return Proxy&TModel + * @phpstan-return Proxy + * + * @throws \RuntimeException If no entities exist + */ + final public static function last(string $sortedField = 'id'): Proxy + { + if (null === $proxy = static::repository()->last($sortedField)) { + throw new \RuntimeException(\sprintf('No "%s" objects persisted.', static::class())); + } + + return $proxy; + } + + /** + * @see ProxyRepositoryDecorator::random() + * + * @return Proxy&TModel + * @phpstan-return Proxy + */ + final public static function random(array $attributes = []): Proxy + { + return static::repository()->random($attributes); + } + + /** + * Fetch one random object and create a new object if none exists. + * + * @return Proxy&TModel + * @phpstan-return Proxy + */ + final public static function randomOrCreate(array $attributes = []): Proxy + { + try { + return static::repository()->random($attributes); + } catch (\RuntimeException) { + return static::new()->create($attributes); + } + } + + /** + * @see ProxyRepositoryDecorator::randomSet() + * + * @return list> + * @phpstan-return list> + */ + final public static function randomSet(int $number, array $attributes = []): array + { + return static::repository()->randomSet($number, $attributes); + } + + /** + * @see ProxyRepositoryDecorator::randomRange() + * + * @return list> + * @phpstan-return list> + */ + final public static function randomRange(int $min, int $max, array $attributes = []): array + { + return static::repository()->randomRange($min, $max, $attributes); + } + + /** + * @see ProxyRepositoryDecorator::findAll() + * + * @return list> + * @phpstan-return list> + */ + final public static function all(): array + { + return static::repository()->findAll(); + } + + /** + * @see ProxyRepositoryDecorator::find() + * + * @phpstan-param Proxy|array|mixed $criteria + * @phpstan-return Proxy + * + * @return Proxy&TModel + * + * @throws \RuntimeException If no entity found + */ + final public static function find($criteria): Proxy + { + if (null === $proxy = static::repository()->find($criteria)) { + throw new \RuntimeException(\sprintf('Could not find "%s" object.', static::class())); + } + + return $proxy; + } + + /** + * @see ProxyRepositoryDecorator::findBy() + * + * @return list> + * @phpstan-return list> + */ + final public static function findBy(array $attributes): array + { + return static::repository()->findBy($attributes); + } + + /** + * @phpstan-return ProxyRepositoryDecorator + */ + final public static function repository(): ProxyRepositoryDecorator + { + return static::configuration()->repositoryFor(static::class(), proxy: true); + } +} diff --git a/src/Persistence/PostPersistCallback.php b/src/Persistence/PostPersistCallback.php index 0f05ffd14..894544423 100644 --- a/src/Persistence/PostPersistCallback.php +++ b/src/Persistence/PostPersistCallback.php @@ -13,8 +13,6 @@ namespace Zenstruck\Foundry\Persistence; -use Zenstruck\Foundry\Proxy; - /** * @internal */ diff --git a/src/Persistence/Proxy.php b/src/Persistence/Proxy.php new file mode 100644 index 000000000..7a0041558 --- /dev/null +++ b/src/Persistence/Proxy.php @@ -0,0 +1,125 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Persistence; + +/** + * @template TProxiedObject of object + * + * @mixin TProxiedObject + * + * @author Kevin Bond + * + * @final + */ +interface Proxy +{ + /** + * @return TProxiedObject + */ + public function _real(): object; + + public function _save(): static; + + public function _delete(): static; + + public function _refresh(): static; + + public function _set(string $property, mixed $value): static; + + public function _get(string $property): mixed; + + /** + * @return ProxyRepositoryDecorator + */ + public function _repository(): ProxyRepositoryDecorator; + + public function _enableAutoRefresh(): static; + + public function _disableAutoRefresh(): static; + + /** + * Ensures "autoRefresh" is disabled when executing $callback. Re-enables + * "autoRefresh" after executing callback if it was enabled. + * + * @param callable $callback (object|Proxy $object): void + */ + public function _withoutAutoRefresh(callable $callback): static; + + /** + * @return TProxiedObject + * + * @deprecated Use method "_real()" instead + */ + public function object(): object; + + /** + * @deprecated Use method "_save()" instead + */ + public function save(): static; + + /** + * @deprecated Use method "_delete()" instead + */ + public function remove(): static; + + /** + * @deprecated Use method "_refresh()" instead + */ + public function refresh(): static; + + /** + * @deprecated Use method "_set()" instead + */ + public function forceSet(string $property, mixed $value): static; + + /** + * @deprecated without replacement + */ + public function forceSetAll(array $properties): static; + + /** + * @deprecated Use method "_get()" instead + */ + public function forceGet(string $property): mixed; + + /** + * @deprecated Use method "_repository()" instead + */ + public function repository(): RepositoryDecorator; + + /** + * @deprecated Use method "_enableAutoRefresh()" instead + */ + public function enableAutoRefresh(): static; + + /** + * @deprecated Use method "_disableAutoRefresh()" instead + */ + public function disableAutoRefresh(): static; + + /** + * @param callable $callback (object|Proxy $object): void + * + * @deprecated Use method "_withoutAutoRefresh()" instead + */ + public function withoutAutoRefresh(callable $callback): static; + + /** + * @deprecated without replacement + */ + public function assertPersisted(string $message = '{entity} is not persisted.'): self; + + /** + * @deprecated without replacement + */ + public function assertNotPersisted(string $message = '{entity} is persisted but it should not be.'): self; +} diff --git a/src/Persistence/ProxyRepositoryDecorator.php b/src/Persistence/ProxyRepositoryDecorator.php new file mode 100644 index 000000000..1a071461d --- /dev/null +++ b/src/Persistence/ProxyRepositoryDecorator.php @@ -0,0 +1,436 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Persistence; + +use Doctrine\ODM\MongoDB\DocumentManager; +use Doctrine\ODM\MongoDB\Mapping\ClassMetadata as ODMClassMetadata; +use Doctrine\ORM\EntityManagerInterface; +use Doctrine\ORM\EntityRepository; +use Doctrine\ORM\Mapping\ClassMetadata as ORMClassMetadata; +use Doctrine\ORM\Mapping\MappingException as ORMMappingException; +use Doctrine\Persistence\Mapping\MappingException; +use Doctrine\Persistence\ObjectManager; +use Doctrine\Persistence\ObjectRepository; +use Symfony\Component\PropertyAccess\PropertyAccess; +use Zenstruck\Foundry\Factory; +use Zenstruck\Foundry\Proxy as ProxyObject; + +/** + * @mixin EntityRepository + * @extends RepositoryDecorator + * @template TProxiedObject of object + * + * @author Kevin Bond + * + * @final + */ +class ProxyRepositoryDecorator extends RepositoryDecorator +{ + /** + * @return list>|Proxy + */ + public function __call(string $method, array $arguments) + { + return $this->proxyResult($this->inner()->{$method}(...$arguments)); + } + + public function getIterator(): \Traversable + { + // TODO: $this->inner() is set to ObjectRepository, which is not + // iterable. Can this every be another RepositoryDecorator? + if (\is_iterable($this->inner())) { + return yield from $this->inner(); + } + + yield from $this->findAll(); + } + + /** + * @deprecated use RepositoryDecorator::count() + */ + public function getCount(): int + { + trigger_deprecation('zenstruck\foundry', '1.5.0', 'Using RepositoryDecorator::getCount() is deprecated, use RepositoryDecorator::count() (it is now Countable).'); + + return $this->count(); + } + + /** + * @deprecated use RepositoryDecorator::assert()->empty() + */ + public function assertEmpty(string $message = ''): self + { + trigger_deprecation('zenstruck\foundry', '1.8.0', 'Using RepositoryDecorator::assertEmpty() is deprecated, use RepositoryDecorator::assert()->empty().'); + + $this->assert()->empty($message); + + return $this; + } + + /** + * @deprecated use RepositoryDecorator::assert()->count() + */ + public function assertCount(int $expectedCount, string $message = ''): self + { + trigger_deprecation('zenstruck\foundry', '1.8.0', 'Using RepositoryDecorator::assertCount() is deprecated, use RepositoryDecorator::assert()->count().'); + + $this->assert()->count($expectedCount, $message); + + return $this; + } + + /** + * @deprecated use RepositoryDecorator::assert()->countGreaterThan() + */ + public function assertCountGreaterThan(int $expected, string $message = ''): self + { + trigger_deprecation('zenstruck\foundry', '1.8.0', 'Using RepositoryDecorator::assertCountGreaterThan() is deprecated, use RepositoryDecorator::assert()->countGreaterThan().'); + + $this->assert()->countGreaterThan($expected, $message); + + return $this; + } + + /** + * @deprecated use RepositoryDecorator::assert()->countGreaterThanOrEqual() + */ + public function assertCountGreaterThanOrEqual(int $expected, string $message = ''): self + { + trigger_deprecation('zenstruck\foundry', '1.8.0', 'Using RepositoryDecorator::assertCountGreaterThanOrEqual() is deprecated, use RepositoryDecorator::assert()->countGreaterThanOrEqual().'); + + $this->assert()->countGreaterThanOrEqual($expected, $message); + + return $this; + } + + /** + * @deprecated use RepositoryDecorator::assert()->countLessThan() + */ + public function assertCountLessThan(int $expected, string $message = ''): self + { + trigger_deprecation('zenstruck\foundry', '1.8.0', 'Using RepositoryDecorator::assertCountLessThan() is deprecated, use RepositoryDecorator::assert()->countLessThan().'); + + $this->assert()->countLessThan($expected, $message); + + return $this; + } + + /** + * @deprecated use RepositoryDecorator::assert()->countLessThanOrEqual() + */ + public function assertCountLessThanOrEqual(int $expected, string $message = ''): self + { + trigger_deprecation('zenstruck\foundry', '1.8.0', 'Using RepositoryDecorator::assertCountLessThanOrEqual() is deprecated, use RepositoryDecorator::assert()->countLessThanOrEqual().'); + + $this->assert()->countLessThanOrEqual($expected, $message); + + return $this; + } + + /** + * @deprecated use RepositoryDecorator::assert()->exists() + * @phpstan-param Proxy|array|mixed $criteria + */ + public function assertExists($criteria, string $message = ''): self + { + trigger_deprecation('zenstruck\foundry', '1.8.0', 'Using RepositoryDecorator::assertExists() is deprecated, use RepositoryDecorator::assert()->exists().'); + + $this->assert()->exists($criteria, $message); + + return $this; + } + + /** + * @deprecated use RepositoryDecorator::assert()->notExists() + * @phpstan-param Proxy|array|mixed $criteria + */ + public function assertNotExists($criteria, string $message = ''): self + { + trigger_deprecation('zenstruck\foundry', '1.8.0', 'Using RepositoryDecorator::assertNotExists() is deprecated, use RepositoryDecorator::assert()->notExists().'); + + $this->assert()->notExists($criteria, $message); + + return $this; + } + + /** + * @return (Proxy&TProxiedObject)|null + * + * @phpstan-return Proxy|null + */ + public function first(string $sortedField = 'id'): ?Proxy + { + return $this->findBy([], [$sortedField => 'ASC'], 1)[0] ?? null; + } + + /** + * @return (Proxy&TProxiedObject)|null + * + * @phpstan-return Proxy|null + */ + public function last(string $sortedField = 'id'): ?Proxy + { + return $this->findBy([], [$sortedField => 'DESC'], 1)[0] ?? null; + } + + /** + * Remove all rows. + */ + public function truncate(): void + { + $om = $this->getObjectManager(); + + if ($om instanceof EntityManagerInterface) { + $om->createQuery("DELETE {$this->getClassName()} e")->execute(); + + return; + } + + if ($om instanceof DocumentManager) { + $om->getDocumentCollection($this->getClassName())->deleteMany([]); + } + } + + /** + * Fetch one random object. + * + * @param array $attributes The findBy criteria + * + * @return Proxy&TProxiedObject + * + * @throws \RuntimeException if no objects are persisted + * + * @phpstan-return Proxy + */ + public function random(array $attributes = []): Proxy + { + return $this->randomSet(1, $attributes)[0]; + } + + /** + * Fetch a random set of objects. + * + * @param int $number The number of objects to return + * @param array $attributes The findBy criteria + * + * @return list> + * + * @throws \RuntimeException if not enough persisted objects to satisfy the number requested + * @throws \InvalidArgumentException if number is less than zero + */ + public function randomSet(int $number, array $attributes = []): array + { + if ($number < 0) { + throw new \InvalidArgumentException(\sprintf('$number must be positive (%d given).', $number)); + } + + return $this->randomRange($number, $number, $attributes); + } + + /** + * Fetch a random range of objects. + * + * @param int $min The minimum number of objects to return + * @param int $max The maximum number of objects to return + * @param array $attributes The findBy criteria + * + * @return list> + * + * @throws \RuntimeException if not enough persisted objects to satisfy the max + * @throws \InvalidArgumentException if min is less than zero + * @throws \InvalidArgumentException if max is less than min + */ + public function randomRange(int $min, int $max, array $attributes = []): array + { + if ($min < 0) { + throw new \InvalidArgumentException(\sprintf('$min must be positive (%d given).', $min)); + } + + if ($max < $min) { + throw new \InvalidArgumentException(\sprintf('$max (%d) cannot be less than $min (%d).', $max, $min)); + } + + $all = \array_values($this->findBy($attributes)); + + \shuffle($all); + + if (\count($all) < $max) { + throw new \RuntimeException(\sprintf('At least %d "%s" object(s) must have been persisted (%d persisted).', $max, $this->getClassName(), \count($all))); + } + + return \array_slice($all, 0, \random_int($min, $max)); // @phpstan-ignore-line + } + + /** + * @param object|array|mixed $criteria + * + * @return (Proxy&TProxiedObject)|null + * + * @phpstan-param Proxy|array|mixed $criteria + * @phpstan-return Proxy|null + */ + public function find($criteria) + { + if ($criteria instanceof Proxy) { + $criteria = $criteria->_real(); + } + + if (!\is_array($criteria)) { + /** @var TProxiedObject|null $result */ + $result = $this->inner()->find($criteria); + + return $this->proxyResult($result); + } + + $normalizedCriteria = []; + $propertyAccessor = PropertyAccess::createPropertyAccessor(); + foreach ($criteria as $attributeName => $attributeValue) { + if (!\is_object($attributeValue)) { + $normalizedCriteria[$attributeName] = $attributeValue; + + continue; + } + + if ($attributeValue instanceof Factory) { + $attributeValue = $attributeValue->withoutPersisting()->createAndUproxify(); + } elseif ($attributeValue instanceof Proxy) { + $attributeValue = $attributeValue->_real(); + } + + try { + $metadataForAttribute = $this->getObjectManager()->getClassMetadata($attributeValue::class); + } catch (MappingException|ORMMappingException) { + $normalizedCriteria[$attributeName] = $attributeValue; + + continue; + } + + $isEmbedded = match ($metadataForAttribute::class) { + ORMClassMetadata::class => $metadataForAttribute->isEmbeddedClass, + ODMClassMetadata::class => $metadataForAttribute->isEmbeddedDocument, + default => throw new \LogicException(\sprintf('Metadata class %s is not supported.', $metadataForAttribute::class)), + }; + + // it's a regular entity + if (!$isEmbedded) { + $normalizedCriteria[$attributeName] = $attributeValue; + + continue; + } + + foreach ($metadataForAttribute->getFieldNames() as $field) { + $embeddableFieldValue = $propertyAccessor->getValue($attributeValue, $field); + if (\is_object($embeddableFieldValue)) { + throw new \InvalidArgumentException('Nested embeddable objects are still not supported in "find()" method.'); + } + + $normalizedCriteria["{$attributeName}.{$field}"] = $embeddableFieldValue; + } + } + + return $this->findOneBy($normalizedCriteria); + } + + /** + * @return list> + */ + public function findAll(): array + { + return $this->proxyResult($this->inner()->findAll()); + } + + /** + * @param int|null $limit + * @param int|null $offset + * + * @return list> + */ + public function findBy(array $criteria, ?array $orderBy = null, $limit = null, $offset = null): array + { + return $this->proxyResult($this->inner()->findBy(self::normalizeCriteria($criteria), $orderBy, $limit, $offset)); + } + + /** + * @param array|null $orderBy Some ObjectRepository's (ie Doctrine\ORM\EntityRepository) add this optional parameter + * + * @return (Proxy&TProxiedObject)|null + * + * @throws \RuntimeException if the wrapped ObjectRepository does not have the $orderBy parameter + * + * @phpstan-return Proxy|null + */ + public function findOneBy(array $criteria, ?array $orderBy = null): ?Proxy + { + if (null !== $orderBy) { + trigger_deprecation('zenstruck\foundry', '1.38.0', 'Argument "$orderBy" of method "%s()" is deprecated and will be removed in Foundry 2.0. Use "%s::findBy()" instead if you need an order.', __METHOD__, __CLASS__); + } + + if (\is_array($orderBy)) { + $wrappedParams = (new \ReflectionClass($this->inner()))->getMethod('findOneBy')->getParameters(); + + if (!isset($wrappedParams[1]) || 'orderBy' !== $wrappedParams[1]->getName() || !($type = $wrappedParams[1]->getType()) instanceof \ReflectionNamedType || 'array' !== $type->getName()) { + throw new \RuntimeException(\sprintf('Wrapped repository\'s (%s) findOneBy method does not have an $orderBy parameter.', $this->inner()::class)); + } + } + + /** @var TProxiedObject|null $result */ + $result = $this->inner()->findOneBy(self::normalizeCriteria($criteria), $orderBy); // @phpstan-ignore-line + if (null === $result) { + return null; + } + + return $this->proxyResult($result); + } + + /** + * @return class-string + */ + public function getClassName(): string + { + return $this->inner()->getClassName(); + } + + /** + * @param TProxiedObject|list|null $result + * + * @return Proxy|Proxy[]|object|object[]|mixed + * + * @phpstan-return ($result is array ? list> : Proxy) + */ + private function proxyResult(mixed $result) + { + if (\is_array($result)) { + return \array_map(fn(mixed $o): mixed => $this->proxyResult($o), $result); + } + + if ($result && \is_a($result, $this->getClassName())) { + return ProxyObject::createFromPersisted($result); + } + + return $result; + } + + private static function normalizeCriteria(array $criteria): array + { + return \array_map( + static fn($value) => $value instanceof Proxy ? $value->_real() : $value, + $criteria, + ); + } + + private function getObjectManager(): ObjectManager + { + return Factory::configuration()->objectManagerFor($this->getClassName()); + } +} + +\class_exists(\Zenstruck\Foundry\RepositoryProxy::class); diff --git a/src/Persistence/RepositoryAssertions.php b/src/Persistence/RepositoryAssertions.php new file mode 100644 index 000000000..c54556798 --- /dev/null +++ b/src/Persistence/RepositoryAssertions.php @@ -0,0 +1,142 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Persistence; + +use Zenstruck\Assert; + +/** + * @author Kevin Bond + * + * @final + */ +class RepositoryAssertions +{ + private static string $countWithoutCriteriaDeprecationMessagePattern = 'Passing the message to %s() as second parameter is deprecated. Use third parameter.'; + + public function __construct(private RepositoryDecorator $repository) + { + } + + public function empty(array|string $criteria = [], string $message = 'Expected {entity} repository to be empty but it has {actual} items.'): self + { + if (\is_string($criteria)) { + trigger_deprecation('zenstruck/foundry', '1.26', 'Passing the message to %s() as first parameter is deprecated. Use second parameter.', __METHOD__); + + $message = $criteria; + } + + return $this->count(0, \is_array($criteria) ? $criteria : [], $message); + } + + public function count(int $expectedCount, array|string $criteria = [], string $message = 'Expected count of {entity} repository ({actual}) to be {expected}.'): self + { + if (\is_string($criteria)) { + trigger_deprecation('zenstruck/foundry', '1.26', self::$countWithoutCriteriaDeprecationMessagePattern, __METHOD__); + + $message = $criteria; + } + + Assert::that($this->repository->count(\is_array($criteria) ? $criteria : [])) + ->is($expectedCount, $message, ['entity' => $this->repository->getClassName()]) + ; + + return $this; + } + + public function countGreaterThan(int $expected, array|string $criteria = [], string $message = 'Expected count of {entity} repository ({actual}) to be greater than {expected}.'): self + { + if (\is_string($criteria)) { + trigger_deprecation('zenstruck/foundry', '1.26', self::$countWithoutCriteriaDeprecationMessagePattern, __METHOD__); + + $message = $criteria; + } + + Assert::that($this->repository->count(\is_array($criteria) ? $criteria : [])) + ->isGreaterThan($expected, $message, ['entity' => $this->repository->getClassName()]) + ; + + return $this; + } + + public function countGreaterThanOrEqual(int $expected, array|string $criteria = [], string $message = 'Expected count of {entity} repository ({actual}) to be greater than or equal to {expected}.'): self + { + if (\is_string($criteria)) { + trigger_deprecation('zenstruck/foundry', '1.26', self::$countWithoutCriteriaDeprecationMessagePattern, __METHOD__); + + $message = $criteria; + } + + Assert::that($this->repository->count(\is_array($criteria) ? $criteria : [])) + ->isGreaterThanOrEqualTo($expected, $message, ['entity' => $this->repository->getClassName()]) + ; + + return $this; + } + + public function countLessThan(int $expected, array|string $criteria = [], string $message = 'Expected count of {entity} repository ({actual}) to be less than {expected}.'): self + { + if (\is_string($criteria)) { + trigger_deprecation('zenstruck/foundry', '1.26', self::$countWithoutCriteriaDeprecationMessagePattern, __METHOD__); + + $message = $criteria; + } + + Assert::that($this->repository->count(\is_array($criteria) ? $criteria : [])) + ->isLessThan($expected, $message, ['entity' => $this->repository->getClassName()]) + ; + + return $this; + } + + public function countLessThanOrEqual(int $expected, array|string $criteria = [], string $message = 'Expected count of {entity} repository ({actual}) to be less than or equal to {expected}.'): self + { + if (\is_string($criteria)) { + trigger_deprecation('zenstruck/foundry', '1.26', self::$countWithoutCriteriaDeprecationMessagePattern, __METHOD__); + + $message = $criteria; + } + + Assert::that($this->repository->count(\is_array($criteria) ? $criteria : [])) + ->isLessThanOrEqualTo($expected, $message, ['entity' => $this->repository->getClassName()]) + ; + + return $this; + } + + /** + * @param object|array|mixed $criteria + */ + public function exists($criteria, string $message = 'Expected {entity} to exist but it does not.'): self + { + Assert::that($this->repository->find($criteria))->isNotEmpty($message, [ + 'entity' => $this->repository->getClassName(), + 'criteria' => $criteria, + ]); + + return $this; + } + + /** + * @param object|array|mixed $criteria + */ + public function notExists($criteria, string $message = 'Expected {entity} to not exist but it does.'): self + { + Assert::that($this->repository->find($criteria))->isEmpty($message, [ + 'entity' => $this->repository->getClassName(), + 'criteria' => $criteria, + ]); + + return $this; + } +} + +\class_exists(\Zenstruck\Foundry\RepositoryAssertions::class); diff --git a/src/Persistence/RepositoryDecorator.php b/src/Persistence/RepositoryDecorator.php new file mode 100644 index 000000000..e1c6e120c --- /dev/null +++ b/src/Persistence/RepositoryDecorator.php @@ -0,0 +1,328 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Persistence; + +use Doctrine\ODM\MongoDB\DocumentManager; +use Doctrine\ODM\MongoDB\Mapping\ClassMetadata as ODMClassMetadata; +use Doctrine\ORM\EntityManagerInterface; +use Doctrine\ORM\EntityRepository; +use Doctrine\ORM\Mapping\ClassMetadata as ORMClassMetadata; +use Doctrine\ORM\Mapping\MappingException as ORMMappingException; +use Doctrine\Persistence\Mapping\MappingException; +use Doctrine\Persistence\ObjectManager; +use Doctrine\Persistence\ObjectRepository; +use Symfony\Component\PropertyAccess\PropertyAccess; +use Zenstruck\Foundry\Factory; + +/** + * @mixin EntityRepository + * @template TObject of object + * + * @author Kevin Bond + * + * @final + */ +class RepositoryDecorator implements ObjectRepository, \IteratorAggregate, \Countable +{ + /** + * @param ObjectRepository $repository + */ + public function __construct(private ObjectRepository $repository) + { + } + + /** + * @return list|TObject + */ + public function __call(string $method, array $arguments) + { + return $this->repository->{$method}(...$arguments); + } + + /** + * @return ObjectRepository + */ + public function inner(): ObjectRepository + { + return $this->repository; + } + + public function count(array $criteria = []): int + { + if ($this->repository instanceof EntityRepository) { + // use query to avoid loading all entities + return $this->repository->count($criteria); + } + + return \count($this->findBy($criteria)); + } + + public function getIterator(): \Traversable + { + // TODO: $this->repository is set to ObjectRepository, which is not + // iterable. Can this every be another RepositoryDecorator? + if (\is_iterable($this->repository)) { + return yield from $this->repository; + } + + yield from $this->findAll(); + } + + public function assert(): RepositoryAssertions + { + return new RepositoryAssertions($this); + } + + /** + * @return TObject|null + */ + public function first(string $sortedField = 'id'): ?object + { + return $this->findBy([], [$sortedField => 'ASC'], 1)[0] ?? null; + } + + /** + * @return TObject|null + */ + public function last(string $sortedField = 'id'): ?object + { + return $this->findBy([], [$sortedField => 'DESC'], 1)[0] ?? null; + } + + /** + * Remove all rows. + */ + public function truncate(): void + { + $om = $this->getObjectManager(); + + if ($om instanceof EntityManagerInterface) { + $om->createQuery("DELETE {$this->getClassName()} e")->execute(); + + return; + } + + if ($om instanceof DocumentManager) { + $om->getDocumentCollection($this->getClassName())->deleteMany([]); + } + } + + /** + * Fetch one random object. + * + * @param array $attributes The findBy criteria + * + * @return TObject + * + * @throws \RuntimeException if no objects are persisted + */ + public function random(array $attributes = []): object + { + return $this->randomSet(1, $attributes)[0]; + } + + /** + * Fetch a random set of objects. + * + * @param int $number The number of objects to return + * @param array $attributes The findBy criteria + * + * @return list + * + * @throws \RuntimeException if not enough persisted objects to satisfy the number requested + * @throws \InvalidArgumentException if number is less than zero + */ + public function randomSet(int $number, array $attributes = []): array + { + if ($number < 0) { + throw new \InvalidArgumentException(\sprintf('$number must be positive (%d given).', $number)); + } + + return $this->randomRange($number, $number, $attributes); + } + + /** + * Fetch a random range of objects. + * + * @param int $min The minimum number of objects to return + * @param int $max The maximum number of objects to return + * @param array $attributes The findBy criteria + * + * @return list + * + * @throws \RuntimeException if not enough persisted objects to satisfy the max + * @throws \InvalidArgumentException if min is less than zero + * @throws \InvalidArgumentException if max is less than min + */ + public function randomRange(int $min, int $max, array $attributes = []): array + { + if ($min < 0) { + throw new \InvalidArgumentException(\sprintf('$min must be positive (%d given).', $min)); + } + + if ($max < $min) { + throw new \InvalidArgumentException(\sprintf('$max (%d) cannot be less than $min (%d).', $max, $min)); + } + + $all = \array_values($this->findBy($attributes)); + + \shuffle($all); + + if (\count($all) < $max) { + throw new \RuntimeException(\sprintf('At least %d "%s" object(s) must have been persisted (%d persisted).', $max, $this->getClassName(), \count($all))); + } + + return \array_slice($all, 0, \random_int($min, $max)); // @phpstan-ignore-line + } + + /** + * @param object|array|mixed $criteria + * + * @return TObject|null + * + * @phpstan-param TObject|array|mixed $criteria + * @phpstan-return TObject|null + */ + public function find($criteria) + { + if ($criteria instanceof Proxy) { + $criteria = $criteria->_real(); + } + + if (!\is_array($criteria)) { + /** @var TObject|null $result */ + $result = $this->repository->find($criteria); + + return $result; + } + + $normalizedCriteria = []; + $propertyAccessor = PropertyAccess::createPropertyAccessor(); + foreach ($criteria as $attributeName => $attributeValue) { + if (!\is_object($attributeValue)) { + $normalizedCriteria[$attributeName] = $attributeValue; + + continue; + } + + if ($attributeValue instanceof Factory) { + $attributeValue = $attributeValue->withoutPersisting()->createAndUproxify(); + } elseif ($attributeValue instanceof Proxy) { + $attributeValue = $attributeValue->_real(); + } + + try { + $metadataForAttribute = $this->getObjectManager()->getClassMetadata($attributeValue::class); + } catch (MappingException|ORMMappingException) { + $normalizedCriteria[$attributeName] = $attributeValue; + + continue; + } + + $isEmbedded = match ($metadataForAttribute::class) { + ORMClassMetadata::class => $metadataForAttribute->isEmbeddedClass, + ODMClassMetadata::class => $metadataForAttribute->isEmbeddedDocument, + default => throw new \LogicException(\sprintf('Metadata class %s is not supported.', $metadataForAttribute::class)), + }; + + // it's a regular entity + if (!$isEmbedded) { + $normalizedCriteria[$attributeName] = $attributeValue; + + continue; + } + + foreach ($metadataForAttribute->getFieldNames() as $field) { + $embeddableFieldValue = $propertyAccessor->getValue($attributeValue, $field); + if (\is_object($embeddableFieldValue)) { + throw new \InvalidArgumentException('Nested embeddable objects are still not supported in "find()" method.'); + } + + $normalizedCriteria["{$attributeName}.{$field}"] = $embeddableFieldValue; + } + } + + return $this->findOneBy($normalizedCriteria); + } + + /** + * @return list + */ + public function findAll(): array + { + return $this->repository->findAll(); + } + + /** + * @param int|null $limit + * @param int|null $offset + * + * @return list + */ + public function findBy(array $criteria, ?array $orderBy = null, $limit = null, $offset = null): array + { + return $this->repository->findBy(self::normalizeCriteria($criteria), $orderBy, $limit, $offset); + } + + /** + * @param array|null $orderBy Some ObjectRepository's (ie Doctrine\ORM\EntityRepository) add this optional parameter + * + * @return TObject|null + * + * @throws \RuntimeException if the wrapped ObjectRepository does not have the $orderBy parameter + */ + public function findOneBy(array $criteria, ?array $orderBy = null): ?object + { + if (null !== $orderBy) { + trigger_deprecation('zenstruck\foundry', '1.38.0', 'Argument "$orderBy" of method "%s()" is deprecated and will be removed in Foundry 2.0. Use "%s::findBy()" instead if you need an order.', __METHOD__, __CLASS__); + } + + if (\is_array($orderBy)) { + $wrappedParams = (new \ReflectionClass($this->repository))->getMethod('findOneBy')->getParameters(); + + if (!isset($wrappedParams[1]) || 'orderBy' !== $wrappedParams[1]->getName() || !($type = $wrappedParams[1]->getType()) instanceof \ReflectionNamedType || 'array' !== $type->getName()) { + throw new \RuntimeException(\sprintf('Wrapped repository\'s (%s) findOneBy method does not have an $orderBy parameter.', $this->repository::class)); + } + } + + /** @var TObject|null $result */ + $result = $this->repository->findOneBy(self::normalizeCriteria($criteria), $orderBy); // @phpstan-ignore-line + if (null === $result) { + return null; + } + + return $result; + } + + /** + * @return class-string + */ + public function getClassName(): string + { + return $this->repository->getClassName(); + } + + private static function normalizeCriteria(array $criteria): array + { + return \array_map( + static fn($value) => $value instanceof Proxy ? $value->_real() : $value, + $criteria, + ); + } + + private function getObjectManager(): ObjectManager + { + return Factory::configuration()->objectManagerFor($this->getClassName()); + } +} diff --git a/src/Persistence/functions.php b/src/Persistence/functions.php new file mode 100644 index 000000000..d0d1bba5e --- /dev/null +++ b/src/Persistence/functions.php @@ -0,0 +1,139 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Persistence; + +use Zenstruck\Foundry\AnonymousFactoryGenerator; +use Zenstruck\Foundry\Configuration; +use Zenstruck\Foundry\Factory; +use Zenstruck\Foundry\Proxy as LegacyProxy; + +/** + * @param class-string $class + * + * @return RepositoryDecorator + * @see Configuration::repositoryFor() + * + * @template TObject of object + */ +function repository(string $class): RepositoryDecorator +{ + return Factory::configuration()->repositoryFor($class, proxy: false); +} + +/** + * @param class-string $class + * + * @return ProxyRepositoryDecorator + * @see Configuration::repositoryFor() + * + * @template TObject of object + */ +function proxy_repository(string $class): ProxyRepositoryDecorator +{ + return Factory::configuration()->repositoryFor($class, proxy: true); +} + +/** + * @return TObject + * + * @template TObject of object + * @phpstan-param class-string $class + * @see Factory::create() + */ +function persist(string $class, array|callable $attributes = []): object +{ + return persistent_factory($class)->create($attributes); +} + +/** + * @return Proxy + * + * @template TObject of object + * @phpstan-param class-string $class + * @see Factory::create() + */ +function persist_proxy(string $class, array|callable $attributes = []): Proxy +{ + return proxy_factory($class)->create($attributes); +} + +/** + * Create an anonymous "persistent" factory for the given class. + * + * @template T of object + * + * @param class-string $class + * @param array|callable(int):array $attributes + * + * @return PersistentObjectFactory + */ +function persistent_factory(string $class, array|callable $attributes = []): PersistentObjectFactory +{ + return AnonymousFactoryGenerator::create($class, PersistentObjectFactory::class)::new($attributes); +} + +/** + * Create an anonymous "persistent with proxy" factory for the given class. + * + * @template T of object + * + * @param class-string $class + * @param array|callable(int):array $attributes + * + * @return PersistentProxyObjectFactory + */ +function proxy_factory(string $class, array|callable $attributes = []): PersistentProxyObjectFactory +{ + if ((new \ReflectionClass($class))->isFinal()) { + throw new \RuntimeException(\sprintf('Cannot create PersistentProxyObjectFactory for final class "%s". Pass parameter "$withProxy" to false instead, or unfinalize "%1$s" class.', $class)); + } + + return AnonymousFactoryGenerator::create($class, PersistentProxyObjectFactory::class)::new($attributes); +} + +/** + * Create an auto-refreshable proxy for the object. + * + * @template T of object + * + * @param T $object + * + * @return Proxy + */ +function proxy(object $object): object +{ + return new LegacyProxy($object); +} + +/** + * @param callable():void $callback + */ +function flush_after(callable $callback): void +{ + Factory::configuration()->delayFlush($callback); +} + +/** + * Disable persisting factories globally. + */ +function disable_persisting(): void +{ + Factory::configuration()->disablePersist(); +} + +/** + * Enable persisting factories globally. + */ +function enable_persisting(): void +{ + Factory::configuration()->enablePersist(); +} diff --git a/src/Proxy.php b/src/Proxy.php index 1b8e2c413..3a1715a35 100644 --- a/src/Proxy.php +++ b/src/Proxy.php @@ -17,14 +17,21 @@ use Zenstruck\Assert; use Zenstruck\Callback; use Zenstruck\Callback\Parameter; +use Zenstruck\Foundry\Persistence\PersistentProxyObjectFactory; +use Zenstruck\Foundry\Persistence\Proxy as ProxyBase; +use Zenstruck\Foundry\Persistence\ProxyRepositoryDecorator; /** + * @deprecated Typehint Zenstruck\Foundry\Persistence\Proxy instead + * * @template TProxiedObject of object + * @implements ProxyBase + * * @mixin TProxiedObject * * @author Kevin Bond */ -final class Proxy implements \Stringable +final class Proxy implements \Stringable, ProxyBase { /** * @phpstan-var class-string @@ -44,44 +51,58 @@ public function __construct( /** @param TProxiedObject $object */ private object $object, ) { + if ((new \ReflectionClass($object::class))->isFinal()) { + trigger_deprecation( + 'zenstruck\foundry', '1.38.0', + 'Using a proxy factory with a final class is deprecated and will throw an error in Foundry 2.0. Use "%s" instead (don\'t forget to remember all ->object() calls!), or unfinalize "%s" class.', + PersistentProxyObjectFactory::class, + $object::class, + ); + } + + if ((new \ReflectionClass($object::class))->isAnonymous()) { + trigger_deprecation( + 'zenstruck\foundry', '1.38.0', + 'Using a proxy factory with an anonymous class is deprecated and will throw an error in Foundry 2.0. Use "%s" instead.', + ObjectFactory::class, + $object::class, + ); + } + $this->class = $object::class; $this->autoRefresh = Factory::configuration()->defaultProxyAutoRefresh(); } public function __call(string $method, array $arguments) // @phpstan-ignore-line { - return $this->object()->{$method}(...$arguments); + return $this->_real()->{$method}(...$arguments); } public function __get(string $name): mixed { - return $this->object()->{$name}; + return $this->_real()->{$name}; } public function __set(string $name, mixed $value): void { - $this->object()->{$name} = $value; + $this->_real()->{$name} = $value; } public function __unset(string $name): void { - unset($this->object()->{$name}); + unset($this->_real()->{$name}); } public function __isset(string $name): bool { - return isset($this->object()->{$name}); + return isset($this->_real()->{$name}); } public function __toString(): string { - $object = $this->object(); + $object = $this->_real(); if (!\method_exists($object, '__toString')) { - if (\PHP_VERSION_ID < 70400) { - return '(no __toString)'; - } - throw new \RuntimeException(\sprintf('Proxied object "%s" cannot be converted to a string.', $this->class)); } @@ -93,7 +114,7 @@ public function __toString(): string * * @template TObject of object * @phpstan-param TObject $object - * @phpstan-return Proxy + * @phpstan-return ProxyBase */ public static function createFromPersisted(object $object): self { @@ -103,43 +124,55 @@ public static function createFromPersisted(object $object): self return $proxy; } - public function isPersisted(): bool + /** + * @deprecated without replacement + */ + public function isPersisted(bool $calledInternally = false): bool { + if (!$calledInternally) { + trigger_deprecation('zenstruck\foundry', '1.38.0', 'Method "%s()" is deprecated and will be removed in 2.0 without replacement.', __METHOD__); + } + return $this->persisted; } /** * @return TProxiedObject + * + * @deprecated Use method "_real()" instead */ public function object(): object + { + trigger_deprecation('zenstruck\foundry', '1.38.0', 'Method "%s()" is deprecated and will be removed in 2.0. Use "%s::_real()" instead.', __METHOD__, self::class); + + return $this->_real(); + } + + public function _real(): object { if (!$this->autoRefresh || !$this->persisted || !Factory::configuration()->isFlushingEnabled() || !Factory::configuration()->isPersistEnabled()) { return $this->object; } - $om = $this->objectManager(); - - // only check for changes if the object is managed in the current om - if (($om instanceof EntityManagerInterface || $om instanceof DocumentManager) && $om->contains($this->object)) { - // cannot use UOW::recomputeSingleEntityChangeSet() here as it wrongly computes embedded objects as changed - $om->getUnitOfWork()->computeChangeSet($om->getClassMetadata($this->class), $this->object); - - if ( - ($om instanceof EntityManagerInterface && !empty($om->getUnitOfWork()->getEntityChangeSet($this->object))) - || ($om instanceof DocumentManager && !empty($om->getUnitOfWork()->getDocumentChangeSet($this->object)))) { - throw new \RuntimeException(\sprintf('Cannot auto refresh "%s" as there are unsaved changes. Be sure to call ->save() or disable auto refreshing (see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#auto-refresh for details).', $this->class)); - } + try { + $this->_autoRefresh(); + } catch (\Throwable) { } - $this->refresh(); - return $this->object; } /** - * @phpstan-return static + * @deprecated Use method "_save()" instead */ - public function save(): self + public function save(): static + { + trigger_deprecation('zenstruck\foundry', '1.38.0', 'Method "%s()" is deprecated and will be removed in 2.0. Use "%s::_save()" instead.', __METHOD__, self::class); + + return $this->_save(); + } + + public function _save(): static { $this->objectManager()->persist($this->object); @@ -152,7 +185,17 @@ public function save(): self return $this; } - public function remove(): self + /** + * @deprecated Use method "_delete()" instead + */ + public function remove(): static + { + trigger_deprecation('zenstruck\foundry', '1.38.0', 'Method "%s()" is deprecated and will be removed in 2.0. Use "%s::_delete()" instead.', __METHOD__, self::class); + + return $this->_delete(); + } + + public function _delete(): static { $this->objectManager()->remove($this->object); $this->objectManager()->flush(); @@ -162,7 +205,17 @@ public function remove(): self return $this; } - public function refresh(): self + /** + * @deprecated Use method "_refresh()" instead + */ + public function refresh(): static + { + trigger_deprecation('zenstruck\foundry', '1.38.0', 'Method "%s()" is deprecated and will be removed in 2.0. Use "%s::_refresh()" instead.', __METHOD__, self::class); + + return $this->_refresh(); + } + + public function _refresh(): static { if (!Factory::configuration()->isPersistEnabled()) { return $this; @@ -172,6 +225,20 @@ public function refresh(): self throw new \RuntimeException(\sprintf('Cannot refresh unpersisted object (%s).', $this->class)); } + $om = $this->objectManager(); + + // only check for changes if the object is managed in the current om + if (($om instanceof EntityManagerInterface || $om instanceof DocumentManager) && !$om->getUnitOfWork()->isScheduledForInsert($this->object) && $om->contains($this->object)) { + // cannot use UOW::recomputeSingleEntityChangeSet() here as it wrongly computes embedded objects as changed + $om->getUnitOfWork()->computeChangeSet($om->getClassMetadata($this->class), $this->object); + + if ( + ($om instanceof EntityManagerInterface && !empty($om->getUnitOfWork()->getEntityChangeSet($this->object))) + || ($om instanceof DocumentManager && !empty($om->getUnitOfWork()->getDocumentChangeSet($this->object)))) { + throw new \RuntimeException(\sprintf('Cannot auto refresh "%s" as there are unsaved changes. Be sure to call ->save() or disable auto refreshing (see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#auto-refresh for details).', $this->class)); + } + } + if ($this->objectManager()->contains($this->object)) { $this->objectManager()->refresh($this->object); @@ -187,36 +254,85 @@ public function refresh(): self return $this; } - public function forceSet(string $property, mixed $value): self + /** + * @deprecated Use method "_set()" instead + */ + public function forceSet(string $property, mixed $value): static + { + trigger_deprecation('zenstruck\foundry', '1.38.0', 'Method "%s()" is deprecated and will be removed in 2.0. Use "%s::_set()" instead.', __METHOD__, self::class); + + return $this->_set($property, $value); + } + + public function _set(string $property, mixed $value): static { - return $this->forceSetAll([$property => $value]); + $this->_autoRefresh(); + $object = $this->_real(); + + Instantiator::forceSet($object, $property, $value, calledInternally: true); + + return $this; } - public function forceSetAll(array $properties): self + /** + * @deprecated without replacement + */ + public function forceSetAll(array $properties): static { - $object = $this->object(); + trigger_deprecation('zenstruck\foundry', '1.38.0', 'Method "%s()" is deprecated and will be removed in 2.0 without replacement.', __METHOD__); + + $object = $this->_real(); foreach ($properties as $property => $value) { - Instantiator::forceSet($object, $property, $value); + Instantiator::forceSet($object, $property, $value, calledInternally: true); } return $this; } /** - * @return mixed + * @deprecated Use method "_get()" instead */ - public function forceGet(string $property) + public function forceGet(string $property): mixed + { + trigger_deprecation('zenstruck\foundry', '1.38.0', 'Method "%s()" is deprecated and will be removed in 2.0. Use "%s::_get()" instead.', __METHOD__, self::class); + + return $this->_get($property); + } + + public function _get(string $property): mixed + { + $this->_autoRefresh(); + + return Instantiator::forceGet($this->_real(), $property, calledInternally: true); + } + + /** + * @deprecated Use method "_repository()" instead + */ + public function repository(): ProxyRepositoryDecorator + { + trigger_deprecation('zenstruck\foundry', '1.38.0', 'Method "%s()" is deprecated and will be removed in 2.0. Use "%s::_repository()" instead.', __METHOD__, self::class); + + return $this->_repository(); + } + + public function _repository(): ProxyRepositoryDecorator { - return Instantiator::forceGet($this->object(), $property); + return Factory::configuration()->repositoryFor($this->class, proxy: true); } - public function repository(): RepositoryProxy + /** + * @deprecated Use method "_enableAutoRefresh()" instead + */ + public function enableAutoRefresh(): static { - return Factory::configuration()->repositoryFor($this->class); + trigger_deprecation('zenstruck\foundry', '1.38.0', 'Method "%s()" is deprecated and will be removed in 2.0. Use "%s::_enableAutoRefresh()" instead.', __METHOD__, self::class); + + return $this->_enableAutoRefresh(); } - public function enableAutoRefresh(): self + public function _enableAutoRefresh(): static { if (!$this->persisted) { throw new \RuntimeException(\sprintf('Cannot enable auto-refresh on unpersisted object (%s).', $this->class)); @@ -227,7 +343,17 @@ public function enableAutoRefresh(): self return $this; } - public function disableAutoRefresh(): self + /** + * @deprecated Use method "_disableAutoRefresh()" instead + */ + public function disableAutoRefresh(): static + { + trigger_deprecation('zenstruck\foundry', '1.38.0', 'Method "%s()" is deprecated and will be removed in 2.0. Use "%s::_disableAutoRefresh()" instead.', __METHOD__, self::class); + + return $this->_disableAutoRefresh(); + } + + public function _disableAutoRefresh(): static { $this->autoRefresh = false; @@ -235,14 +361,18 @@ public function disableAutoRefresh(): self } /** - * Ensures "autoRefresh" is disabled when executing $callback. Re-enables - * "autoRefresh" after executing callback if it was enabled. - * * @param callable $callback (object|Proxy $object): void * - * @phpstan-return static + * @deprecated Use method "_withoutAutoRefresh()" instead */ - public function withoutAutoRefresh(callable $callback): self + public function withoutAutoRefresh(callable $callback): static + { + trigger_deprecation('zenstruck\foundry', '1.38.0', 'Method "%s()" is deprecated and will be removed in 2.0. Use "%s::_withoutAutoRefresh()" instead.', __METHOD__, self::class); + + return $this->_withoutAutoRefresh($callback); + } + + public function _withoutAutoRefresh(callable $callback): static { $original = $this->autoRefresh; $this->autoRefresh = false; @@ -254,15 +384,25 @@ public function withoutAutoRefresh(callable $callback): self return $this; } + /** + * @deprecated without replacement + */ public function assertPersisted(string $message = '{entity} is not persisted.'): self { + trigger_deprecation('zenstruck\foundry', '1.38.0', 'Method "%s()" is deprecated and will be removed in 2.0 without replacement.', __METHOD__); + Assert::that($this->fetchObject())->isNotEmpty($message, ['entity' => $this->class]); return $this; } + /** + * @deprecated without replacement + */ public function assertNotPersisted(string $message = '{entity} is persisted but it should not be.'): self { + trigger_deprecation('zenstruck\foundry', '1.38.0', 'Method "%s()" is deprecated and will be removed in 2.0 without replacement.', __METHOD__); + Assert::that($this->fetchObject())->isEmpty($message, ['entity' => $this->class]); return $this; @@ -276,8 +416,8 @@ public function executeCallback(callable $callback, mixed ...$arguments): void Callback::createFor($callback)->invoke( Parameter::union( Parameter::untyped($this), - Parameter::typed(self::class, $this), - Parameter::typed($this->class, Parameter::factory(fn(): object => $this->object())), + Parameter::typed(ProxyBase::class, $this), + Parameter::typed($this->class, Parameter::factory(fn(): object => $this->_real())), )->optional(), ...$arguments, ); @@ -310,4 +450,13 @@ private function objectManager(): ObjectManager return Factory::configuration()->objectManagerFor($this->class); } + + private function _autoRefresh(): void + { + if (!$this->autoRefresh) { + return; + } + + $this->_refresh(); + } } diff --git a/src/RepositoryAssertions.php b/src/RepositoryAssertions.php index bfbb91e7e..2557844f6 100644 --- a/src/RepositoryAssertions.php +++ b/src/RepositoryAssertions.php @@ -11,128 +11,25 @@ namespace Zenstruck\Foundry; -use Zenstruck\Assert; - -/** - * @author Kevin Bond - */ -final class RepositoryAssertions -{ - private static string $countWithoutCriteriaDeprecationMessagePattern = 'Passing the message to %s() as second parameter is deprecated. Use third parameter.'; - - public function __construct(private RepositoryProxy $repository) - { - } - - public function empty(array|string $criteria = [], string $message = 'Expected {entity} repository to be empty but it has {actual} items.'): self - { - if (\is_string($criteria)) { - trigger_deprecation('zenstruck/foundry', '1.26', 'Passing the message to %s() as first parameter is deprecated. Use second parameter.', __METHOD__); - - $message = $criteria; - } - - return $this->count(0, \is_array($criteria) ? $criteria : [], $message); - } - - public function count(int $expectedCount, array|string $criteria = [], string $message = 'Expected count of {entity} repository ({actual}) to be {expected}.'): self - { - if (\is_string($criteria)) { - trigger_deprecation('zenstruck/foundry', '1.26', self::$countWithoutCriteriaDeprecationMessagePattern, __METHOD__); - - $message = $criteria; - } - - Assert::that($this->repository->count(\is_array($criteria) ? $criteria : [])) - ->is($expectedCount, $message, ['entity' => $this->repository->getClassName()]) - ; - - return $this; - } - - public function countGreaterThan(int $expected, array|string $criteria = [], string $message = 'Expected count of {entity} repository ({actual}) to be greater than {expected}.'): self - { - if (\is_string($criteria)) { - trigger_deprecation('zenstruck/foundry', '1.26', self::$countWithoutCriteriaDeprecationMessagePattern, __METHOD__); - - $message = $criteria; - } - - Assert::that($this->repository->count(\is_array($criteria) ? $criteria : [])) - ->isGreaterThan($expected, $message, ['entity' => $this->repository->getClassName()]) - ; - - return $this; - } - - public function countGreaterThanOrEqual(int $expected, array|string $criteria = [], string $message = 'Expected count of {entity} repository ({actual}) to be greater than or equal to {expected}.'): self - { - if (\is_string($criteria)) { - trigger_deprecation('zenstruck/foundry', '1.26', self::$countWithoutCriteriaDeprecationMessagePattern, __METHOD__); - - $message = $criteria; - } - - Assert::that($this->repository->count(\is_array($criteria) ? $criteria : [])) - ->isGreaterThanOrEqualTo($expected, $message, ['entity' => $this->repository->getClassName()]) - ; - - return $this; - } - - public function countLessThan(int $expected, array|string $criteria = [], string $message = 'Expected count of {entity} repository ({actual}) to be less than {expected}.'): self - { - if (\is_string($criteria)) { - trigger_deprecation('zenstruck/foundry', '1.26', self::$countWithoutCriteriaDeprecationMessagePattern, __METHOD__); - - $message = $criteria; - } - - Assert::that($this->repository->count(\is_array($criteria) ? $criteria : [])) - ->isLessThan($expected, $message, ['entity' => $this->repository->getClassName()]) - ; - - return $this; - } - - public function countLessThanOrEqual(int $expected, array|string $criteria = [], string $message = 'Expected count of {entity} repository ({actual}) to be less than or equal to {expected}.'): self - { - if (\is_string($criteria)) { - trigger_deprecation('zenstruck/foundry', '1.26', self::$countWithoutCriteriaDeprecationMessagePattern, __METHOD__); - - $message = $criteria; - } - - Assert::that($this->repository->count(\is_array($criteria) ? $criteria : [])) - ->isLessThanOrEqualTo($expected, $message, ['entity' => $this->repository->getClassName()]) - ; - - return $this; - } - - /** - * @param object|array|mixed $criteria - */ - public function exists($criteria, string $message = 'Expected {entity} to exist but it does not.'): self - { - Assert::that($this->repository->find($criteria))->isNotEmpty($message, [ - 'entity' => $this->repository->getClassName(), - 'criteria' => $criteria, - ]); +use Zenstruck\Foundry\Persistence\RepositoryAssertions as NewRepositoryAssertions; + +if (!\class_exists(NewRepositoryAssertions::class, false)) { + trigger_deprecation( + 'zenstruck\foundry', + '1.38.0', + 'Class "%s" is deprecated and will be removed in version 2.0. Use "%s" instead.', + RepositoryAssertions::class, + NewRepositoryAssertions::class, + ); +} - return $this; - } +\class_alias(NewRepositoryAssertions::class, RepositoryAssertions::class); +if (false) { /** - * @param object|array|mixed $criteria + * @deprecated */ - public function notExists($criteria, string $message = 'Expected {entity} to not exist but it does.'): self + final class RepositoryAssertions extends NewRepositoryAssertions { - Assert::that($this->repository->find($criteria))->isEmpty($message, [ - 'entity' => $this->repository->getClassName(), - 'criteria' => $criteria, - ]); - - return $this; } } diff --git a/src/RepositoryProxy.php b/src/RepositoryProxy.php index d4a027045..80a3e1df3 100644 --- a/src/RepositoryProxy.php +++ b/src/RepositoryProxy.php @@ -11,445 +11,33 @@ namespace Zenstruck\Foundry; -use Doctrine\ODM\MongoDB\DocumentManager; -use Doctrine\ODM\MongoDB\Mapping\ClassMetadata as ODMClassMetadata; -use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityRepository; -use Doctrine\ORM\Mapping\ClassMetadata as ORMClassMetadata; -use Doctrine\ORM\Mapping\MappingException as ORMMappingException; -use Doctrine\Persistence\Mapping\MappingException; -use Doctrine\Persistence\ObjectManager; -use Doctrine\Persistence\ObjectRepository; -use Symfony\Component\PropertyAccess\PropertyAccess; - -/** - * @mixin EntityRepository - * @template TProxiedObject of object - * - * @author Kevin Bond - */ -final class RepositoryProxy implements ObjectRepository, \IteratorAggregate, \Countable -{ - /** - * @param ObjectRepository $repository - */ - public function __construct(private ObjectRepository $repository) - { - } - - /** - * @return list>|Proxy - */ - public function __call(string $method, array $arguments) - { - return $this->proxyResult($this->repository->{$method}(...$arguments)); - } - - /** - * @return ObjectRepository - */ - public function inner(): ObjectRepository - { - return $this->repository; - } - - public function count(array $criteria = []): int - { - if ($this->repository instanceof EntityRepository) { - // use query to avoid loading all entities - return $this->repository->count($criteria); - } - - return \count($this->findBy($criteria)); - } - - public function getIterator(): \Traversable - { - // TODO: $this->repository is set to ObjectRepository, which is not - // iterable. Can this every be another RepositoryProxy? - if (\is_iterable($this->repository)) { - return yield from $this->repository; - } - - yield from $this->findAll(); - } - - /** - * @deprecated use RepositoryProxy::count() - */ - public function getCount(): int - { - trigger_deprecation('zenstruck\foundry', '1.5.0', 'Using RepositoryProxy::getCount() is deprecated, use RepositoryProxy::count() (it is now Countable).'); - - return $this->count(); - } - - public function assert(): RepositoryAssertions - { - return new RepositoryAssertions($this); - } - - /** - * @deprecated use RepositoryProxy::assert()->empty() - */ - public function assertEmpty(string $message = ''): self - { - trigger_deprecation('zenstruck\foundry', '1.8.0', 'Using RepositoryProxy::assertEmpty() is deprecated, use RepositoryProxy::assert()->empty().'); - - $this->assert()->empty($message); - - return $this; - } - - /** - * @deprecated use RepositoryProxy::assert()->count() - */ - public function assertCount(int $expectedCount, string $message = ''): self - { - trigger_deprecation('zenstruck\foundry', '1.8.0', 'Using RepositoryProxy::assertCount() is deprecated, use RepositoryProxy::assert()->count().'); - - $this->assert()->count($expectedCount, $message); - - return $this; - } - - /** - * @deprecated use RepositoryProxy::assert()->countGreaterThan() - */ - public function assertCountGreaterThan(int $expected, string $message = ''): self - { - trigger_deprecation('zenstruck\foundry', '1.8.0', 'Using RepositoryProxy::assertCountGreaterThan() is deprecated, use RepositoryProxy::assert()->countGreaterThan().'); - - $this->assert()->countGreaterThan($expected, $message); - - return $this; - } - - /** - * @deprecated use RepositoryProxy::assert()->countGreaterThanOrEqual() - */ - public function assertCountGreaterThanOrEqual(int $expected, string $message = ''): self - { - trigger_deprecation('zenstruck\foundry', '1.8.0', 'Using RepositoryProxy::assertCountGreaterThanOrEqual() is deprecated, use RepositoryProxy::assert()->countGreaterThanOrEqual().'); - - $this->assert()->countGreaterThanOrEqual($expected, $message); - - return $this; - } - - /** - * @deprecated use RepositoryProxy::assert()->countLessThan() - */ - public function assertCountLessThan(int $expected, string $message = ''): self - { - trigger_deprecation('zenstruck\foundry', '1.8.0', 'Using RepositoryProxy::assertCountLessThan() is deprecated, use RepositoryProxy::assert()->countLessThan().'); - - $this->assert()->countLessThan($expected, $message); - - return $this; - } - - /** - * @deprecated use RepositoryProxy::assert()->countLessThanOrEqual() - */ - public function assertCountLessThanOrEqual(int $expected, string $message = ''): self - { - trigger_deprecation('zenstruck\foundry', '1.8.0', 'Using RepositoryProxy::assertCountLessThanOrEqual() is deprecated, use RepositoryProxy::assert()->countLessThanOrEqual().'); - - $this->assert()->countLessThanOrEqual($expected, $message); - - return $this; - } - - /** - * @deprecated use RepositoryProxy::assert()->exists() - * @phpstan-param Proxy|array|mixed $criteria - */ - public function assertExists($criteria, string $message = ''): self - { - trigger_deprecation('zenstruck\foundry', '1.8.0', 'Using RepositoryProxy::assertExists() is deprecated, use RepositoryProxy::assert()->exists().'); - - $this->assert()->exists($criteria, $message); - - return $this; - } - - /** - * @deprecated use RepositoryProxy::assert()->notExists() - * @phpstan-param Proxy|array|mixed $criteria - */ - public function assertNotExists($criteria, string $message = ''): self - { - trigger_deprecation('zenstruck\foundry', '1.8.0', 'Using RepositoryProxy::assertNotExists() is deprecated, use RepositoryProxy::assert()->notExists().'); - - $this->assert()->notExists($criteria, $message); - - return $this; - } - - /** - * @return (Proxy&TProxiedObject)|null - * - * @phpstan-return Proxy|null - */ - public function first(string $sortedField = 'id'): ?Proxy - { - return $this->findBy([], [$sortedField => 'ASC'], 1)[0] ?? null; - } - - /** - * @return (Proxy&TProxiedObject)|null - * - * @phpstan-return Proxy|null - */ - public function last(string $sortedField = 'id'): ?Proxy - { - return $this->findBy([], [$sortedField => 'DESC'], 1)[0] ?? null; - } - - /** - * Remove all rows. - */ - public function truncate(): void - { - $om = $this->getObjectManager(); - - if ($om instanceof EntityManagerInterface) { - $om->createQuery("DELETE {$this->getClassName()} e")->execute(); - - return; - } - - if ($om instanceof DocumentManager) { - $om->getDocumentCollection($this->getClassName())->deleteMany([]); - } - } - - /** - * Fetch one random object. - * - * @param array $attributes The findBy criteria - * - * @return Proxy&TProxiedObject - * - * @throws \RuntimeException if no objects are persisted - * - * @phpstan-return Proxy - */ - public function random(array $attributes = []): Proxy - { - return $this->randomSet(1, $attributes)[0]; - } - - /** - * Fetch a random set of objects. - * - * @param int $number The number of objects to return - * @param array $attributes The findBy criteria - * - * @return list> - * - * @throws \RuntimeException if not enough persisted objects to satisfy the number requested - * @throws \InvalidArgumentException if number is less than zero - */ - public function randomSet(int $number, array $attributes = []): array - { - if ($number < 0) { - throw new \InvalidArgumentException(\sprintf('$number must be positive (%d given).', $number)); - } - - return $this->randomRange($number, $number, $attributes); - } - - /** - * Fetch a random range of objects. - * - * @param int $min The minimum number of objects to return - * @param int $max The maximum number of objects to return - * @param array $attributes The findBy criteria - * - * @return list> - * - * @throws \RuntimeException if not enough persisted objects to satisfy the max - * @throws \InvalidArgumentException if min is less than zero - * @throws \InvalidArgumentException if max is less than min - */ - public function randomRange(int $min, int $max, array $attributes = []): array - { - if ($min < 0) { - throw new \InvalidArgumentException(\sprintf('$min must be positive (%d given).', $min)); - } - - if ($max < $min) { - throw new \InvalidArgumentException(\sprintf('$max (%d) cannot be less than $min (%d).', $max, $min)); - } - - $all = \array_values($this->findBy($attributes)); - - \shuffle($all); - - if (\count($all) < $max) { - throw new \RuntimeException(\sprintf('At least %d "%s" object(s) must have been persisted (%d persisted).', $max, $this->getClassName(), \count($all))); - } - - return \array_slice($all, 0, \random_int($min, $max)); // @phpstan-ignore-line - } - - /** - * @param object|array|mixed $criteria - * - * @return (Proxy&TProxiedObject)|null - * - * @phpstan-param Proxy|array|mixed $criteria - * @phpstan-return Proxy|null - */ - public function find($criteria) - { - if ($criteria instanceof Proxy) { - $criteria = $criteria->object(); - } - - if (!\is_array($criteria)) { - /** @var TProxiedObject|null $result */ - $result = $this->repository->find($criteria); - - return $this->proxyResult($result); - } - - $normalizedCriteria = []; - $propertyAccessor = PropertyAccess::createPropertyAccessor(); - foreach ($criteria as $attributeName => $attributeValue) { - if (!\is_object($attributeValue)) { - $normalizedCriteria[$attributeName] = $attributeValue; - - continue; - } - - if ($attributeValue instanceof Factory) { - $attributeValue = $attributeValue->withoutPersisting()->create()->object(); - } elseif ($attributeValue instanceof Proxy) { - $attributeValue = $attributeValue->object(); - } - - try { - $metadataForAttribute = $this->getObjectManager()->getClassMetadata($attributeValue::class); - } catch (MappingException|ORMMappingException) { - $normalizedCriteria[$attributeName] = $attributeValue; - - continue; - } - - $isEmbedded = match ($metadataForAttribute::class) { - ORMClassMetadata::class => $metadataForAttribute->isEmbeddedClass, - ODMClassMetadata::class => $metadataForAttribute->isEmbeddedDocument, - default => throw new \LogicException(\sprintf('Metadata class %s is not supported.', $metadataForAttribute::class)), - }; - - // it's a regular entity - if (!$isEmbedded) { - $normalizedCriteria[$attributeName] = $attributeValue; - - continue; - } - - foreach ($metadataForAttribute->getFieldNames() as $field) { - $embeddableFieldValue = $propertyAccessor->getValue($attributeValue, $field); - if (\is_object($embeddableFieldValue)) { - throw new \InvalidArgumentException('Nested embeddable objects are still not supported in "find()" method.'); - } - - $normalizedCriteria["{$attributeName}.{$field}"] = $embeddableFieldValue; - } - } - - return $this->findOneBy($normalizedCriteria); - } - - /** - * @return list> - */ - public function findAll(): array - { - return $this->proxyResult($this->repository->findAll()); - } +use Zenstruck\Foundry\Persistence\ProxyRepositoryDecorator; + +if (!\class_exists(ProxyRepositoryDecorator::class, false)) { + trigger_deprecation( + 'zenstruck\foundry', + '1.38.0', + 'Class "%s" is deprecated and will be removed in version 2.0. Use "%s" instead.', + RepositoryProxy::class, + ProxyRepositoryDecorator::class, + ); +} - /** - * @param int|null $limit - * @param int|null $offset - * - * @return list> - */ - public function findBy(array $criteria, ?array $orderBy = null, $limit = null, $offset = null): array - { - return $this->proxyResult($this->repository->findBy(self::normalizeCriteria($criteria), $orderBy, $limit, $offset)); - } +\class_alias(ProxyRepositoryDecorator::class, RepositoryProxy::class); +if (false) { /** - * @param array|null $orderBy Some ObjectRepository's (ie Doctrine\ORM\EntityRepository) add this optional parameter - * - * @return (Proxy&TProxiedObject)|null - * - * @throws \RuntimeException if the wrapped ObjectRepository does not have the $orderBy parameter + * @mixin EntityRepository + * @template TProxiedObject of object * - * @phpstan-return Proxy|null - */ - public function findOneBy(array $criteria, ?array $orderBy = null): ?Proxy - { - if (\is_array($orderBy)) { - $wrappedParams = (new \ReflectionClass($this->repository))->getMethod('findOneBy')->getParameters(); - - if (!isset($wrappedParams[1]) || 'orderBy' !== $wrappedParams[1]->getName() || !($type = $wrappedParams[1]->getType()) instanceof \ReflectionNamedType || 'array' !== $type->getName()) { - throw new \RuntimeException(\sprintf('Wrapped repository\'s (%s) findOneBy method does not have an $orderBy parameter.', $this->repository::class)); - } - } - - /** @var TProxiedObject|null $result */ - $result = $this->repository->findOneBy(self::normalizeCriteria($criteria), $orderBy); // @phpstan-ignore-line - if (null === $result) { - return null; - } - - return $this->proxyResult($result); - } - - /** - * @return class-string - */ - public function getClassName(): string - { - return $this->repository->getClassName(); - } - - /** - * @param TProxiedObject|list|null $result + * @extends ProxyRepositoryDecorator * - * @return Proxy|Proxy[]|object|object[]|mixed + * @deprecated * - * @phpstan-return ($result is array ? list> : Proxy) + * @author Kevin Bond */ - private function proxyResult(mixed $result) - { - if (\is_array($result)) { - return \array_map(fn(mixed $o): mixed => $this->proxyResult($o), $result); - } - - if ($result && \is_a($result, $this->getClassName())) { - return Proxy::createFromPersisted($result); - } - - return $result; - } - - private static function normalizeCriteria(array $criteria): array - { - return \array_map( - static fn($value) => $value instanceof Proxy ? $value->object() : $value, - $criteria, - ); - } - - private function getObjectManager(): ObjectManager + final class RepositoryProxy extends ProxyRepositoryDecorator { - return Factory::configuration()->objectManagerFor($this->getClassName()); } } diff --git a/src/Story.php b/src/Story.php index 5c28fe6e5..3022ade96 100644 --- a/src/Story.php +++ b/src/Story.php @@ -11,6 +11,9 @@ namespace Zenstruck\Foundry; +use Zenstruck\Foundry\Persistence\Proxy; +use Zenstruck\Foundry\Proxy as ProxyObject; + /** * @author Kevin Bond * @@ -177,13 +180,13 @@ private static function normalizeObject(object $object): Proxy } // ensure objects are proxied - if (!$object instanceof Proxy) { - $object = new Proxy($object); + if (!$object instanceof ProxyObject) { + $object = new ProxyObject($object); } // ensure proxies are persisted - if (!$object->isPersisted()) { - $object->save(); + if (!$object->isPersisted(calledInternally: true)) { + $object->_save(); } return $object; diff --git a/src/Test/Factories.php b/src/Test/Factories.php index b6d86c19b..7a5d47436 100644 --- a/src/Test/Factories.php +++ b/src/Test/Factories.php @@ -18,6 +18,9 @@ use Zenstruck\Foundry\Exception\FoundryBootException; use Zenstruck\Foundry\Factory; +use function Zenstruck\Foundry\Persistence\disable_persisting; +use function Zenstruck\Foundry\Persistence\enable_persisting; + /** * @mixin KernelTestCase * @@ -64,20 +67,34 @@ public static function _setUpFactories(): void public static function _tearDownFactories(): void { try { - Factory::configuration()->enablePersist(); + $configuration = Factory::configuration(); + + if ($configuration->hasManagerRegistry()) { + $configuration->enablePersist(); + } } catch (FoundryBootException) { } TestState::shutdownFoundry(); } + /** + * @deprecated + */ public function disablePersist(): void { - Factory::configuration()->disablePersist(); + trigger_deprecation('zenstruck\foundry', '1.38.0', 'Method "%s()" is deprecated and will be removed in Foundry 2.0. Use "Zenstruck\Foundry\Persistence\disable_persisting()" instead.', __METHOD__); + + disable_persisting(); } + /** + * @deprecated + */ public function enablePersist(): void { - Factory::configuration()->enablePersist(); + trigger_deprecation('zenstruck\foundry', '1.38.0', 'Method "%s()" is deprecated and will be removed in Foundry 2.0. Use "Zenstruck\Foundry\Persistence\enable_persisting()" instead.', __METHOD__); + + enable_persisting(); } } diff --git a/src/Test/TestState.php b/src/Test/TestState.php index e16de84b3..2a19230f6 100644 --- a/src/Test/TestState.php +++ b/src/Test/TestState.php @@ -18,7 +18,7 @@ use Zenstruck\Foundry\ChainManagerRegistry; use Zenstruck\Foundry\Configuration; use Zenstruck\Foundry\Factory; -use Zenstruck\Foundry\Instantiator; +use Zenstruck\Foundry\Object\Instantiator; use Zenstruck\Foundry\StoryManager; /** @@ -37,51 +37,51 @@ final class TestState private static array $globalStates = []; /** - * @deprecated Use TestState::configure() + * @deprecated Use UnitTestConfig::configure() */ public static function setInstantiator(callable $instantiator): void { - trigger_deprecation('zenstruck\foundry', '1.23', 'Usage of TestState::setInstantiator() is deprecated. Please use TestState::configure().'); + trigger_deprecation('zenstruck/foundry', '1.38.0', 'Method "%s()" is deprecated and will be removed in Foundry 2.0. Use "%s::configure()" instead.', __METHOD__, UnitTestConfig::class); self::$instantiator = $instantiator; } /** - * @deprecated Use TestState::configure() + * @deprecated Use UnitTestConfig::configure() */ public static function setFaker(Faker\Generator $faker): void { - trigger_deprecation('zenstruck\foundry', '1.23', 'Usage of TestState::setFaker() is deprecated. Please use TestState::configure().'); + trigger_deprecation('zenstruck/foundry', '1.38.0', 'Method "%s()" is deprecated and will be removed in Foundry 2.0. Use "%s::configure()" instead.', __METHOD__, UnitTestConfig::class); self::$faker = $faker; } /** - * @deprecated Use bundle configuration + * @deprecated */ public static function enableDefaultProxyAutoRefresh(): void { - trigger_deprecation('zenstruck\foundry', '1.23', 'Usage of TestState::enableDefaultProxyAutoRefresh() is deprecated. Please use bundle configuration under "auto_refresh_proxies" key.'); + trigger_deprecation('zenstruck/foundry', '1.38.0', 'Method "%s()" is deprecated and will be removed in Foundry 2.0 without replacement.', __METHOD__); self::$defaultProxyAutoRefresh = true; } /** - * @deprecated Use bundle configuration + * @deprecated */ public static function disableDefaultProxyAutoRefresh(): void { - trigger_deprecation('zenstruck\foundry', '1.23', 'Usage of TestState::disableDefaultProxyAutoRefresh() is deprecated. Please use bundle configuration under "auto_refresh_proxies" key.'); + trigger_deprecation('zenstruck/foundry', '1.38.0', 'Method "%s()" is deprecated and will be removed in Foundry 2.0 without replacement.', __METHOD__); self::$defaultProxyAutoRefresh = false; } /** - * @deprecated Use TestState::enableDefaultProxyAutoRefresh() + * @deprecated */ public static function alwaysAutoRefreshProxies(): void { - trigger_deprecation('zenstruck\foundry', '1.9', 'TestState::alwaysAutoRefreshProxies() is deprecated, use TestState::enableDefaultProxyAutoRefresh().'); + trigger_deprecation('zenstruck/foundry', '1.38.0', 'Method "%s()" is deprecated and will be removed in Foundry 2.0 without replacement.', __METHOD__); self::enableDefaultProxyAutoRefresh(); } @@ -146,11 +146,11 @@ public static function shutdownFoundry(): void } /** - * @deprecated use TestState::bootFoundry() + * @deprecated */ public static function bootFactory(Configuration $configuration): Configuration { - trigger_deprecation('zenstruck/foundry', '1.4.0', 'TestState::bootFactory() is deprecated, use TestState::bootFoundry().'); + trigger_deprecation('zenstruck/foundry', '1.38.0', 'Method "%s()" is deprecated and will be removed in Foundry 2.0 without replacement.', __METHOD__); self::bootFoundry($configuration); @@ -220,7 +220,21 @@ public static function flushGlobalState(?GlobalStateRegistry $globalStateRegistr StoryManager::setGlobalState(); } + /** + * @deprecated + */ public static function configure(?Instantiator $instantiator = null, ?Faker\Generator $faker = null): void + { + trigger_deprecation('zenstruck/foundry', '1.38.0', 'Method "%s()" is deprecated and will be removed in Foundry 2.0. Use "%s::configure()" instead.', __METHOD__, UnitTestConfig::class); + + self::$instantiator = $instantiator; + self::$faker = $faker; + } + + /** + * @internal + */ + public static function configureInternal(?Instantiator $instantiator = null, ?Faker\Generator $faker = null): void { self::$instantiator = $instantiator; self::$faker = $faker; diff --git a/src/Test/UnitTestConfig.php b/src/Test/UnitTestConfig.php new file mode 100644 index 000000000..1899bcbd7 --- /dev/null +++ b/src/Test/UnitTestConfig.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Test; + +use Faker; +use Zenstruck\Foundry\Object\Instantiator; + +final class UnitTestConfig +{ + public static function configure(?Instantiator $instantiator = null, ?Faker\Generator $faker = null): void + { + TestState::configureInternal($instantiator, $faker); + } +} diff --git a/src/functions.php b/src/functions.php index e070771df..a6ee93ace 100644 --- a/src/functions.php +++ b/src/functions.php @@ -12,6 +12,13 @@ namespace Zenstruck\Foundry; use Faker; +use Zenstruck\Foundry\Persistence\Proxy; +use Zenstruck\Foundry\Persistence\ProxyRepositoryDecorator; +use Zenstruck\Foundry\Proxy as ProxyObject; + +use function Zenstruck\Foundry\Persistence\persist_proxy; +use function Zenstruck\Foundry\Persistence\persistent_factory; +use function Zenstruck\Foundry\Persistence\proxy_factory; /** * @see Factory::__construct() @@ -42,7 +49,9 @@ function factory(string $class, array|callable $defaultAttributes = []): Anonymo */ function anonymous(string $class, array|callable $defaultAttributes = []): Factory { - return new class($class, $defaultAttributes) extends Factory {}; + trigger_deprecation('zenstruck\foundry', '1.37', 'Usage of "%s()" function is deprecated and will be removed in 2.0. Use the "Zenstruck\Foundry\Persistence\proxy_factory()" function instead.', __FUNCTION__); + + return proxy_factory($class, $defaultAttributes); } /** @@ -53,10 +62,14 @@ function anonymous(string $class, array|callable $defaultAttributes = []): Facto * @template TObject of object * @phpstan-param class-string $class * @phpstan-return Proxy + * + * @deprecated */ function create(string $class, array|callable $attributes = []): Proxy { - return anonymous($class)->create($attributes); + trigger_deprecation('zenstruck\foundry', '1.38.0', 'Function "%s()" is deprecated and will be removed in Foundry 2.0. Use "Zenstruck\Foundry\Persistence\persist_proxy()" instead.', __FUNCTION__); + + return persist_proxy($class, $attributes); } /** @@ -67,10 +80,14 @@ function create(string $class, array|callable $attributes = []): Proxy * @template TObject of object * @phpstan-param class-string $class * @phpstan-return list> + * + * @deprecated */ function create_many(int $number, string $class, array|callable $attributes = []): array { - return anonymous($class)->many($number)->create($attributes); + trigger_deprecation('zenstruck\foundry', '1.38.0', 'Function "%s()" is deprecated and will be removed in Foundry 2.0 without replacement.', __FUNCTION__); + + return proxy_factory($class)->many($number)->create($attributes); } /** @@ -81,10 +98,27 @@ function create_many(int $number, string $class, array|callable $attributes = [] * @template TObject of object * @phpstan-param class-string $class * @phpstan-return Proxy + * + * @deprecated */ function instantiate(string $class, array|callable $attributes = []): Proxy { - return anonymous($class)->withoutPersisting()->create($attributes); + trigger_deprecation('zenstruck\foundry', '1.38.0', 'Function "%s()" is deprecated and will be removed in Foundry 2.0. Use "%s::object()" instead.', __FUNCTION__, __NAMESPACE__); + + return new ProxyObject(object($class, $attributes)); +} + +/** + * Instantiate the given class. + * + * @return TObject "unpersisted" + * + * @template TObject of object + * @phpstan-param class-string $class + */ +function object(string $class, array|callable $attributes = []): object +{ + return persistent_factory($class)->withoutPersisting()->create($attributes); } /** @@ -95,10 +129,14 @@ function instantiate(string $class, array|callable $attributes = []): Proxy * @template TObject of object * @phpstan-param class-string $class * @phpstan-return list> + * + * @deprecated */ function instantiate_many(int $number, string $class, array|callable $attributes = []): array { - return anonymous($class)->withoutPersisting()->many($number)->create($attributes); + trigger_deprecation('zenstruck\foundry', '1.38.0', 'Function "%s()" is deprecated and will be removed in Foundry 2.0 without replacement.', __FUNCTION__); + + return proxy_factory($class)->withoutPersisting()->many($number)->create($attributes); } /** @@ -108,11 +146,21 @@ function instantiate_many(int $number, string $class, array|callable $attributes * * @param TObject|class-string $objectOrClass * - * @return RepositoryProxy + * @return ProxyRepositoryDecorator + * + * @deprecated */ -function repository(object|string $objectOrClass): RepositoryProxy +function repository(object|string $objectOrClass): ProxyRepositoryDecorator { - return Factory::configuration()->repositoryFor($objectOrClass); + trigger_deprecation('zenstruck\foundry', '1.38.0', 'Function "%s()" is deprecated and will be removed in Foundry 2.0. Use "Zenstruck\Foundry\Persistence\repository()" instead.', __FUNCTION__); + + if (\is_object($objectOrClass)) { + trigger_deprecation('zenstruck\foundry', '1.38.0', 'Passing objects to "%s()" is deprecated and will be removed in Foundry 2.0. Pass directly class-string instead.', __FUNCTION__); + + $objectOrClass = $objectOrClass::class; + } + + return \Zenstruck\Foundry\Persistence\proxy_repository($objectOrClass); } /** @@ -120,7 +168,7 @@ function repository(object|string $objectOrClass): RepositoryProxy */ function faker(): Faker\Generator { - return Factory::faker(); + return Factory::configuration()->faker(); } /** diff --git a/tests/Fixtures/Entity/Cascade/Product.php b/tests/Fixtures/Entity/Cascade/Product.php index b2390e46f..c3beb36f5 100644 --- a/tests/Fixtures/Entity/Cascade/Product.php +++ b/tests/Fixtures/Entity/Cascade/Product.php @@ -82,6 +82,7 @@ public function getReview(): ?Review public function setReview(Review $review): void { $this->review = $review; + $review->setProduct($this); } public function getVariants(): Collection @@ -113,6 +114,7 @@ public function addCategory(ProductCategory $category): void { if (!$this->categories->contains($category)) { $this->categories[] = $category; + $category->addProduct($this); } } diff --git a/tests/Fixtures/Entity/Cascade/ProductCategory.php b/tests/Fixtures/Entity/Cascade/ProductCategory.php index 9946b1e46..5196282bd 100644 --- a/tests/Fixtures/Entity/Cascade/ProductCategory.php +++ b/tests/Fixtures/Entity/Cascade/ProductCategory.php @@ -54,4 +54,12 @@ public function getProducts(): Collection { return $this->products; } + + public function addProduct(Product $product): void + { + if (!$this->products->contains($product)) { + $this->products[] = $product; + $product->addCategory($this); + } + } } diff --git a/tests/Fixtures/Entity/Category.php b/tests/Fixtures/Entity/Category.php index 00af529ce..2a04df503 100644 --- a/tests/Fixtures/Entity/Category.php +++ b/tests/Fixtures/Entity/Category.php @@ -95,7 +95,7 @@ public function addSecondaryPost(Post $secondaryPost): void { if (!$this->secondaryPosts->contains($secondaryPost)) { $this->secondaryPosts[] = $secondaryPost; - $secondaryPost->setCategory($this); + $secondaryPost->setSecondaryCategory($this); } } @@ -105,7 +105,7 @@ public function removeSecondaryPost(Post $secondaryPost): void $this->secondaryPosts->removeElement($secondaryPost); // set the owning side to null (unless already changed) if ($secondaryPost->getCategory() === $this) { - $secondaryPost->setCategory(null); + $secondaryPost->setSecondaryCategory(null); } } } diff --git a/tests/Fixtures/Factories/AddressFactory.php b/tests/Fixtures/Factories/AddressFactory.php index 8d212477c..37c728c78 100644 --- a/tests/Fixtures/Factories/AddressFactory.php +++ b/tests/Fixtures/Factories/AddressFactory.php @@ -11,20 +11,20 @@ namespace Zenstruck\Foundry\Tests\Fixtures\Factories; -use Zenstruck\Foundry\ModelFactory; +use Zenstruck\Foundry\Persistence\PersistentObjectFactory; use Zenstruck\Foundry\Tests\Fixtures\Entity\Address; /** * @author Kevin Bond */ -final class AddressFactory extends ModelFactory +final class AddressFactory extends PersistentObjectFactory { - protected static function getClass(): string + public static function class(): string { return Address::class; } - protected function getDefaults(): array + protected function defaults(): array|callable { return ['value' => 'Some address']; } diff --git a/tests/Fixtures/Factories/CategoryFactory.php b/tests/Fixtures/Factories/CategoryFactory.php index f74a2352a..febbbfb92 100644 --- a/tests/Fixtures/Factories/CategoryFactory.php +++ b/tests/Fixtures/Factories/CategoryFactory.php @@ -11,30 +11,30 @@ namespace Zenstruck\Foundry\Tests\Fixtures\Factories; -use Zenstruck\Foundry\Instantiator; -use Zenstruck\Foundry\ModelFactory; +use Zenstruck\Foundry\Object\Instantiator; +use Zenstruck\Foundry\Persistence\PersistentProxyObjectFactory; use Zenstruck\Foundry\Tests\Fixtures\Entity\Category; /** * @author Kevin Bond */ -final class CategoryFactory extends ModelFactory +final class CategoryFactory extends PersistentProxyObjectFactory { - protected static function getClass(): string + public static function class(): string { return Category::class; } - protected function getDefaults(): array + protected function defaults(): array|callable { return ['name' => self::faker()->sentence()]; } - protected function initialize() + protected function initialize(): static { return $this ->instantiateWith( - (new Instantiator())->allowExtraAttributes(['extraPostsBeforeInstantiate', 'extraPostsAfterInstantiate']), + Instantiator::withConstructor()->allowExtra('extraPostsBeforeInstantiate', 'extraPostsAfterInstantiate'), ) ->beforeInstantiate(function(array $attributes): array { if (isset($attributes['extraPostsBeforeInstantiate'])) { diff --git a/tests/Fixtures/Factories/CategoryServiceFactory.php b/tests/Fixtures/Factories/CategoryServiceFactory.php index 04486b7c5..59f296a1a 100644 --- a/tests/Fixtures/Factories/CategoryServiceFactory.php +++ b/tests/Fixtures/Factories/CategoryServiceFactory.php @@ -11,26 +11,26 @@ namespace Zenstruck\Foundry\Tests\Fixtures\Factories; -use Zenstruck\Foundry\ModelFactory; +use Zenstruck\Foundry\Persistence\PersistentProxyObjectFactory; use Zenstruck\Foundry\Tests\Fixtures\Entity\Category; use Zenstruck\Foundry\Tests\Fixtures\Service; /** * @author Kevin Bond */ -final class CategoryServiceFactory extends ModelFactory +final class CategoryServiceFactory extends PersistentProxyObjectFactory { public function __construct(private Service $service) { parent::__construct(); } - protected static function getClass(): string + public static function class(): string { return Category::class; } - protected function getDefaults(): array + protected function defaults(): array|callable { return ['name' => $this->service->name]; } diff --git a/tests/Fixtures/Factories/CommentFactory.php b/tests/Fixtures/Factories/CommentFactory.php index 461c3af8b..cf3f21ee2 100644 --- a/tests/Fixtures/Factories/CommentFactory.php +++ b/tests/Fixtures/Factories/CommentFactory.php @@ -11,12 +11,17 @@ namespace Zenstruck\Foundry\Tests\Fixtures\Factories; -use Zenstruck\Foundry\ModelFactory; +use Zenstruck\Foundry\Persistence\PersistentProxyObjectFactory; use Zenstruck\Foundry\Tests\Fixtures\Entity\Comment; -final class CommentFactory extends ModelFactory +final class CommentFactory extends PersistentProxyObjectFactory { - protected function getDefaults(): array + public static function class(): string + { + return Comment::class; + } + + protected function defaults(): array|callable { return [ 'user' => UserFactory::new(), @@ -25,9 +30,4 @@ protected function getDefaults(): array 'post' => PostFactory::new(), ]; } - - protected static function getClass(): string - { - return Comment::class; - } } diff --git a/tests/Fixtures/Factories/ContactFactory.php b/tests/Fixtures/Factories/ContactFactory.php index 288db5b44..bab278986 100644 --- a/tests/Fixtures/Factories/ContactFactory.php +++ b/tests/Fixtures/Factories/ContactFactory.php @@ -11,20 +11,20 @@ namespace Zenstruck\Foundry\Tests\Fixtures\Factories; -use Zenstruck\Foundry\ModelFactory; +use Zenstruck\Foundry\Persistence\PersistentProxyObjectFactory; use Zenstruck\Foundry\Tests\Fixtures\Entity\Contact; /** * @author Kevin Bond */ -final class ContactFactory extends ModelFactory +final class ContactFactory extends PersistentProxyObjectFactory { - protected static function getClass(): string + public static function class(): string { return Contact::class; } - protected function getDefaults(): array + protected function defaults(): array|callable { return [ 'name' => 'Sally', diff --git a/tests/Fixtures/Factories/EntityForRelationsFactory.php b/tests/Fixtures/Factories/EntityForRelationsFactory.php index 2463dfc52..642d2e325 100644 --- a/tests/Fixtures/Factories/EntityForRelationsFactory.php +++ b/tests/Fixtures/Factories/EntityForRelationsFactory.php @@ -11,17 +11,17 @@ namespace Zenstruck\Foundry\Tests\Fixtures\Factories; -use Zenstruck\Foundry\ModelFactory; +use Zenstruck\Foundry\Persistence\PersistentProxyObjectFactory; use Zenstruck\Foundry\Tests\Fixtures\Entity\EntityForRelations; -class EntityForRelationsFactory extends ModelFactory +class EntityForRelationsFactory extends PersistentProxyObjectFactory { - protected static function getClass(): string + public static function class(): string { return EntityForRelations::class; } - protected function getDefaults(): array + protected function defaults(): array|callable { return []; } diff --git a/tests/Fixtures/Factories/EntityWithEnumFactory.php b/tests/Fixtures/Factories/EntityWithEnumFactory.php index 8afc9416b..58ea9a07a 100644 --- a/tests/Fixtures/Factories/EntityWithEnumFactory.php +++ b/tests/Fixtures/Factories/EntityWithEnumFactory.php @@ -11,21 +11,21 @@ namespace Zenstruck\Foundry\Tests\Fixtures\Factories; -use Zenstruck\Foundry\ModelFactory; +use Zenstruck\Foundry\Persistence\PersistentProxyObjectFactory; use Zenstruck\Foundry\Tests\Fixtures\PHP81\EntityWithEnum; use Zenstruck\Foundry\Tests\Fixtures\PHP81\SomeEnum; -final class EntityWithEnumFactory extends ModelFactory +final class EntityWithEnumFactory extends PersistentProxyObjectFactory { - protected function getDefaults(): array + public static function class(): string { - return [ - 'enum' => self::faker()->randomElement(SomeEnum::cases()), - ]; + return EntityWithEnum::class; } - protected static function getClass(): string + protected function defaults(): array|callable { - return EntityWithEnum::class; + return [ + 'enum' => self::faker()->randomElement(SomeEnum::cases()), + ]; } } diff --git a/tests/Fixtures/Factories/EntityWithPropertyNameDifferentFromConstructFactory.php b/tests/Fixtures/Factories/EntityWithPropertyNameDifferentFromConstructFactory.php index 5e98279da..3ab4dec3e 100644 --- a/tests/Fixtures/Factories/EntityWithPropertyNameDifferentFromConstructFactory.php +++ b/tests/Fixtures/Factories/EntityWithPropertyNameDifferentFromConstructFactory.php @@ -11,13 +11,18 @@ namespace Zenstruck\Foundry\Tests\Fixtures\Factories; -use Zenstruck\Foundry\ModelFactory; +use Zenstruck\Foundry\Persistence\PersistentProxyObjectFactory; use Zenstruck\Foundry\Tests\Fixtures\Entity\EntityWithPropertyNameDifferentFromConstruct; use Zenstruck\Foundry\Tests\Fixtures\Object\SomeObjectFactory; -final class EntityWithPropertyNameDifferentFromConstructFactory extends ModelFactory +final class EntityWithPropertyNameDifferentFromConstructFactory extends PersistentProxyObjectFactory { - protected function getDefaults(): array + public static function class(): string + { + return EntityWithPropertyNameDifferentFromConstruct::class; + } + + protected function defaults(): array|callable { return [ 'scalar' => self::faker()->name(), @@ -26,9 +31,4 @@ protected function getDefaults(): array 'notPersistedObject' => SomeObjectFactory::new(), ]; } - - protected static function getClass(): string - { - return EntityWithPropertyNameDifferentFromConstruct::class; - } } diff --git a/tests/Fixtures/Factories/LegacyPostFactory.php b/tests/Fixtures/Factories/LegacyPostFactory.php new file mode 100644 index 000000000..6fbaf746d --- /dev/null +++ b/tests/Fixtures/Factories/LegacyPostFactory.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Tests\Fixtures\Factories; + +use Zenstruck\Foundry\ModelFactory; +use Zenstruck\Foundry\Tests\Fixtures\Entity\Post; + +/** + * @author Kevin Bond + */ +class LegacyPostFactory extends ModelFactory +{ + protected static function getClass(): string + { + return Post::class; + } + + protected function getDefaults(): array + { + return [ + 'title' => self::faker()->sentence(), + 'body' => self::faker()->sentence(), + ]; + } +} diff --git a/tests/Fixtures/Factories/ODM/CategoryFactory.php b/tests/Fixtures/Factories/ODM/CategoryFactory.php index 615744bb7..2bb656797 100644 --- a/tests/Fixtures/Factories/ODM/CategoryFactory.php +++ b/tests/Fixtures/Factories/ODM/CategoryFactory.php @@ -11,20 +11,20 @@ namespace Zenstruck\Foundry\Tests\Fixtures\Factories\ODM; -use Zenstruck\Foundry\ModelFactory; +use Zenstruck\Foundry\Persistence\PersistentProxyObjectFactory; use Zenstruck\Foundry\Tests\Fixtures\Document\ODMCategory; /** * @author Kevin Bond */ -final class CategoryFactory extends ModelFactory +final class CategoryFactory extends PersistentProxyObjectFactory { - protected static function getClass(): string + public static function class(): string { return ODMCategory::class; } - protected function getDefaults(): array + protected function defaults(): array|callable { return ['name' => self::faker()->sentence()]; } diff --git a/tests/Fixtures/Factories/ODM/CommentFactory.php b/tests/Fixtures/Factories/ODM/CommentFactory.php index 478664e27..4b7cddb01 100644 --- a/tests/Fixtures/Factories/ODM/CommentFactory.php +++ b/tests/Fixtures/Factories/ODM/CommentFactory.php @@ -11,18 +11,18 @@ namespace Zenstruck\Foundry\Tests\Fixtures\Factories\ODM; -use Zenstruck\Foundry\ModelFactory; +use Zenstruck\Foundry\Persistence\PersistentProxyObjectFactory; use Zenstruck\Foundry\Tests\Fixtures\Document\ODMComment; use Zenstruck\Foundry\Tests\Fixtures\Document\ODMUser; -class CommentFactory extends ModelFactory +class CommentFactory extends PersistentProxyObjectFactory { - protected static function getClass(): string + public static function class(): string { return ODMComment::class; } - protected function getDefaults(): array + protected function defaults(): array|callable { return [ 'user' => new ODMUser(self::faker()->userName()), diff --git a/tests/Fixtures/Factories/ODM/PostFactory.php b/tests/Fixtures/Factories/ODM/PostFactory.php index b786f5a6a..ff299b7c5 100644 --- a/tests/Fixtures/Factories/ODM/PostFactory.php +++ b/tests/Fixtures/Factories/ODM/PostFactory.php @@ -12,21 +12,21 @@ namespace Zenstruck\Foundry\Tests\Fixtures\Factories\ODM; use Doctrine\Common\Collections\ArrayCollection; -use Zenstruck\Foundry\ModelFactory; +use Zenstruck\Foundry\Persistence\PersistentProxyObjectFactory; use Zenstruck\Foundry\Tests\Fixtures\Document\ODMComment; use Zenstruck\Foundry\Tests\Fixtures\Document\ODMPost; use Zenstruck\Foundry\Tests\Fixtures\Document\ODMUser; -class PostFactory extends ModelFactory +class PostFactory extends PersistentProxyObjectFactory { public function published(): static { - return $this->addState(static fn(): array => ['published_at' => self::faker()->dateTime()]); + return $this->with(static fn(): array => ['published_at' => self::faker()->dateTime()]); } public function withComments(): static { - return $this->addState(static fn(): array => [ + return $this->with(static fn(): array => [ 'comments' => new ArrayCollection([ new ODMComment(new ODMUser('user'), 'body'), new ODMComment(new ODMUser('user'), 'body'), @@ -34,12 +34,12 @@ public function withComments(): static ]); } - protected static function getClass(): string + public static function class(): string { return ODMPost::class; } - protected function getDefaults(): array + protected function defaults(): array|callable { return [ 'title' => self::faker()->sentence(), diff --git a/tests/Fixtures/Factories/ODM/TagFactory.php b/tests/Fixtures/Factories/ODM/TagFactory.php index 4bdb4c476..8fc227e0c 100644 --- a/tests/Fixtures/Factories/ODM/TagFactory.php +++ b/tests/Fixtures/Factories/ODM/TagFactory.php @@ -11,20 +11,20 @@ namespace Zenstruck\Foundry\Tests\Fixtures\Factories\ODM; -use Zenstruck\Foundry\ModelFactory; +use Zenstruck\Foundry\Persistence\PersistentProxyObjectFactory; use Zenstruck\Foundry\Tests\Fixtures\Document\Tag; /** * @author Kevin Bond */ -final class TagFactory extends ModelFactory +final class TagFactory extends PersistentProxyObjectFactory { - protected static function getClass(): string + public static function class(): string { return Tag::class; } - protected function getDefaults(): array + protected function defaults(): array|callable { return ['name' => self::faker()->sentence()]; } diff --git a/tests/Fixtures/Factories/ODM/UserFactory.php b/tests/Fixtures/Factories/ODM/UserFactory.php index 2f017da36..e44a2f7ab 100644 --- a/tests/Fixtures/Factories/ODM/UserFactory.php +++ b/tests/Fixtures/Factories/ODM/UserFactory.php @@ -11,17 +11,17 @@ namespace Zenstruck\Foundry\Tests\Fixtures\Factories\ODM; -use Zenstruck\Foundry\ModelFactory; +use Zenstruck\Foundry\Persistence\PersistentProxyObjectFactory; use Zenstruck\Foundry\Tests\Fixtures\Document\ODMUser; -final class UserFactory extends ModelFactory +final class UserFactory extends PersistentProxyObjectFactory { - protected static function getClass(): string + public static function class(): string { return ODMUser::class; } - protected function getDefaults(): array + protected function defaults(): array|callable { return []; } diff --git a/tests/Fixtures/Factories/PostFactory.php b/tests/Fixtures/Factories/PostFactory.php index 039632810..67e3a5ea7 100644 --- a/tests/Fixtures/Factories/PostFactory.php +++ b/tests/Fixtures/Factories/PostFactory.php @@ -11,26 +11,31 @@ namespace Zenstruck\Foundry\Tests\Fixtures\Factories; -use Zenstruck\Foundry\Instantiator; -use Zenstruck\Foundry\ModelFactory; +use Zenstruck\Foundry\Object\Instantiator; +use Zenstruck\Foundry\Persistence\PersistentProxyObjectFactory; use Zenstruck\Foundry\Tests\Fixtures\Entity\Post; /** * @author Kevin Bond */ -class PostFactory extends ModelFactory +class PostFactory extends PersistentProxyObjectFactory { public function published(): static + { + return $this->with(static fn(): array => ['published_at' => self::faker()->dateTime()]); + } + + public function publishedWithLegacyMethod(): static { return $this->addState(static fn(): array => ['published_at' => self::faker()->dateTime()]); } - protected static function getClass(): string + public static function class(): string { return Post::class; } - protected function getDefaults(): array + protected function defaults(): array|callable { return [ 'title' => self::faker()->sentence(), @@ -38,11 +43,11 @@ protected function getDefaults(): array ]; } - protected function initialize() + protected function initialize(): static { return $this ->instantiateWith( - (new Instantiator())->allowExtraAttributes(['extraCategoryBeforeInstantiate', 'extraCategoryAfterInstantiate']), + Instantiator::withConstructor()->allowExtra('extraCategoryBeforeInstantiate', 'extraCategoryAfterInstantiate'), ) ->beforeInstantiate(function(array $attributes): array { if (isset($attributes['extraCategoryBeforeInstantiate'])) { diff --git a/tests/Fixtures/Factories/PostFactoryNoProxy.php b/tests/Fixtures/Factories/PostFactoryNoProxy.php new file mode 100644 index 000000000..1b79dbee4 --- /dev/null +++ b/tests/Fixtures/Factories/PostFactoryNoProxy.php @@ -0,0 +1,31 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Tests\Fixtures\Factories; + +use Zenstruck\Foundry\Persistence\PersistentObjectFactory; +use Zenstruck\Foundry\Tests\Fixtures\Entity\Post; + +class PostFactoryNoProxy extends PersistentObjectFactory +{ + public static function class(): string + { + return Post::class; + } + + protected function defaults(): array|callable + { + return [ + 'title' => self::faker()->sentence(), + 'body' => self::faker()->sentence(), + ]; + } +} diff --git a/tests/Fixtures/Factories/PostFactoryWithInvalidInitialize.php b/tests/Fixtures/Factories/PostFactoryWithInvalidInitialize.php index f80e20d15..9cd892b5d 100644 --- a/tests/Fixtures/Factories/PostFactoryWithInvalidInitialize.php +++ b/tests/Fixtures/Factories/PostFactoryWithInvalidInitialize.php @@ -14,7 +14,7 @@ /** * @author Kevin Bond */ -final class PostFactoryWithInvalidInitialize extends PostFactory +final class PostFactoryWithInvalidInitialize extends LegacyPostFactory { protected function initialize() { diff --git a/tests/Fixtures/Factories/PostFactoryWithNullInitialize.php b/tests/Fixtures/Factories/PostFactoryWithNullInitialize.php index 3a3e1ea40..33925e332 100644 --- a/tests/Fixtures/Factories/PostFactoryWithNullInitialize.php +++ b/tests/Fixtures/Factories/PostFactoryWithNullInitialize.php @@ -14,7 +14,7 @@ /** * @author Kevin Bond */ -final class PostFactoryWithNullInitialize extends PostFactory +final class PostFactoryWithNullInitialize extends LegacyPostFactory { protected function initialize() { diff --git a/tests/Fixtures/Factories/PostFactoryWithValidInitialize.php b/tests/Fixtures/Factories/PostFactoryWithValidInitialize.php index 132902116..7de93069f 100644 --- a/tests/Fixtures/Factories/PostFactoryWithValidInitialize.php +++ b/tests/Fixtures/Factories/PostFactoryWithValidInitialize.php @@ -16,7 +16,7 @@ */ final class PostFactoryWithValidInitialize extends PostFactory { - protected function initialize(): self + protected function initialize(): static { return $this->published(); } diff --git a/tests/Fixtures/Factories/SpecificPostFactory.php b/tests/Fixtures/Factories/SpecificPostFactory.php index 911ece5d4..0b41d1102 100644 --- a/tests/Fixtures/Factories/SpecificPostFactory.php +++ b/tests/Fixtures/Factories/SpecificPostFactory.php @@ -18,7 +18,7 @@ */ class SpecificPostFactory extends PostFactory { - protected static function getClass(): string + public static function class(): string { return SpecificPost::class; } diff --git a/tests/Fixtures/Factories/TagFactory.php b/tests/Fixtures/Factories/TagFactory.php index 720951893..ba0a87a00 100644 --- a/tests/Fixtures/Factories/TagFactory.php +++ b/tests/Fixtures/Factories/TagFactory.php @@ -11,20 +11,20 @@ namespace Zenstruck\Foundry\Tests\Fixtures\Factories; -use Zenstruck\Foundry\ModelFactory; +use Zenstruck\Foundry\Persistence\PersistentProxyObjectFactory; use Zenstruck\Foundry\Tests\Fixtures\Entity\Tag; /** * @author Kevin Bond */ -final class TagFactory extends ModelFactory +final class TagFactory extends PersistentProxyObjectFactory { - protected static function getClass(): string + public static function class(): string { return Tag::class; } - protected function getDefaults(): array + protected function defaults(): array|callable { return ['name' => self::faker()->sentence()]; } diff --git a/tests/Fixtures/Factories/UserFactory.php b/tests/Fixtures/Factories/UserFactory.php index 6299fe108..b1eb9f9b9 100644 --- a/tests/Fixtures/Factories/UserFactory.php +++ b/tests/Fixtures/Factories/UserFactory.php @@ -11,20 +11,20 @@ namespace Zenstruck\Foundry\Tests\Fixtures\Factories; -use Zenstruck\Foundry\ModelFactory; +use Zenstruck\Foundry\Persistence\PersistentProxyObjectFactory; use Zenstruck\Foundry\Tests\Fixtures\Entity\User; -final class UserFactory extends ModelFactory +final class UserFactory extends PersistentProxyObjectFactory { - protected function getDefaults(): array + public static function class(): string { - return [ - 'name' => self::faker()->name(), - ]; + return User::class; } - protected static function getClass(): string + protected function defaults(): array|callable { - return User::class; + return [ + 'name' => self::faker()->name(), + ]; } } diff --git a/tests/Fixtures/Kernel.php b/tests/Fixtures/Kernel.php index 8766e2902..549038a3e 100644 --- a/tests/Fixtures/Kernel.php +++ b/tests/Fixtures/Kernel.php @@ -166,7 +166,7 @@ protected function configureContainer(ContainerBuilder $c, LoaderInterface $load } if (\getenv('USE_FOUNDRY_BUNDLE')) { - $foundryConfig = ['auto_refresh_proxies' => false]; + $foundryConfig = []; if ($this->defaultMakeFactoryNamespace) { $foundryConfig['make_factory'] = ['default_namespace' => $this->defaultMakeFactoryNamespace]; } @@ -176,7 +176,7 @@ protected function configureContainer(ContainerBuilder $c, LoaderInterface $load $globalState[] = TagStory::class; $globalState[] = TagStoryAsInvokableService::class; - $foundryConfig['database_resetter'] = ['orm' => ['reset_mode' => $this->ormResetMode]]; + $foundryConfig['orm'] = ['reset' => ['mode' => $this->ormResetMode]]; } if ($this->enableDoctrine && \getenv('MONGO_URL') && !\getenv('USE_DAMA_DOCTRINE_TEST_BUNDLE')) { diff --git a/tests/Fixtures/Maker/expected/can_create_factory.php b/tests/Fixtures/Maker/expected/can_create_factory.php index 9fee8fa04..d8dc13e2a 100644 --- a/tests/Fixtures/Maker/expected/can_create_factory.php +++ b/tests/Fixtures/Maker/expected/can_create_factory.php @@ -12,31 +12,31 @@ namespace App\Factory; use Doctrine\ORM\EntityRepository; -use Zenstruck\Foundry\ModelFactory; -use Zenstruck\Foundry\Proxy; -use Zenstruck\Foundry\RepositoryProxy; +use Zenstruck\Foundry\Persistence\PersistentProxyObjectFactory; +use Zenstruck\Foundry\Persistence\Proxy; +use Zenstruck\Foundry\Persistence\ProxyRepositoryDecorator; use Zenstruck\Foundry\Tests\Fixtures\Entity\Category; /** - * @extends ModelFactory + * @extends PersistentProxyObjectFactory * - * @method Category|Proxy create(array|callable $attributes = []) - * @method static Category|Proxy createOne(array $attributes = []) - * @method static Category|Proxy find(object|array|mixed $criteria) - * @method static Category|Proxy findOrCreate(array $attributes) - * @method static Category|Proxy first(string $sortedField = 'id') - * @method static Category|Proxy last(string $sortedField = 'id') - * @method static Category|Proxy random(array $attributes = []) - * @method static Category|Proxy randomOrCreate(array $attributes = []) - * @method static EntityRepository|RepositoryProxy repository() - * @method static Category[]|Proxy[] all() - * @method static Category[]|Proxy[] createMany(int $number, array|callable $attributes = []) - * @method static Category[]|Proxy[] createSequence(iterable|callable $sequence) - * @method static Category[]|Proxy[] findBy(array $attributes) - * @method static Category[]|Proxy[] randomRange(int $min, int $max, array $attributes = []) - * @method static Category[]|Proxy[] randomSet(int $number, array $attributes = []) + * @method Category|Proxy create(array|callable $attributes = []) + * @method static Category|Proxy createOne(array $attributes = []) + * @method static Category|Proxy find(object|array|mixed $criteria) + * @method static Category|Proxy findOrCreate(array $attributes) + * @method static Category|Proxy first(string $sortedField = 'id') + * @method static Category|Proxy last(string $sortedField = 'id') + * @method static Category|Proxy random(array $attributes = []) + * @method static Category|Proxy randomOrCreate(array $attributes = []) + * @method static EntityRepository|ProxyRepositoryDecorator repository() + * @method static Category[]|Proxy[] all() + * @method static Category[]|Proxy[] createMany(int $number, array|callable $attributes = []) + * @method static Category[]|Proxy[] createSequence(iterable|callable $sequence) + * @method static Category[]|Proxy[] findBy(array $attributes) + * @method static Category[]|Proxy[] randomRange(int $min, int $max, array $attributes = []) + * @method static Category[]|Proxy[] randomSet(int $number, array $attributes = []) */ -final class CategoryFactory extends ModelFactory +final class CategoryFactory extends PersistentProxyObjectFactory { /** * @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#factories-as-services @@ -45,7 +45,11 @@ final class CategoryFactory extends ModelFactory */ public function __construct() { - parent::__construct(); + } + + public static function class(): string + { + return Category::class; } /** @@ -53,7 +57,7 @@ public function __construct() * * @todo add your default values here */ - protected function getDefaults(): array + protected function defaults(): array|callable { return [ 'name' => self::faker()->text(255), @@ -63,15 +67,10 @@ protected function getDefaults(): array /** * @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#initialization */ - protected function initialize(): self + protected function initialize(): static { return $this // ->afterInstantiate(function(Category $category): void {}) ; } - - protected static function getClass(): string - { - return Category::class; - } } diff --git a/tests/Fixtures/Maker/expected/can_create_factory_for_entity_with_repository_with_data_set_phpstan.php b/tests/Fixtures/Maker/expected/can_create_factory_for_entity_with_repository_with_data_set_phpstan.php index ee846db87..ea529b953 100644 --- a/tests/Fixtures/Maker/expected/can_create_factory_for_entity_with_repository_with_data_set_phpstan.php +++ b/tests/Fixtures/Maker/expected/can_create_factory_for_entity_with_repository_with_data_set_phpstan.php @@ -11,48 +11,49 @@ namespace App\Factory; -use Zenstruck\Foundry\ModelFactory; -use Zenstruck\Foundry\Proxy; -use Zenstruck\Foundry\RepositoryProxy; +use Doctrine\ORM\EntityRepository; +use Zenstruck\Foundry\Persistence\PersistentProxyObjectFactory; +use Zenstruck\Foundry\Persistence\Proxy; +use Zenstruck\Foundry\Persistence\ProxyRepositoryDecorator; use Zenstruck\Foundry\Tests\Fixtures\Entity\Post; use Zenstruck\Foundry\Tests\Fixtures\Repository\PostRepository; /** - * @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 = []) * - * @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 = []) + * @phpstan-method Post&Proxy create(array|callable $attributes = []) + * @phpstan-method static Post&Proxy createOne(array $attributes = []) + * @phpstan-method static Post&Proxy find(object|array|mixed $criteria) + * @phpstan-method static Post&Proxy findOrCreate(array $attributes) + * @phpstan-method static Post&Proxy first(string $sortedField = 'id') + * @phpstan-method static Post&Proxy last(string $sortedField = 'id') + * @phpstan-method static Post&Proxy random(array $attributes = []) + * @phpstan-method static Post&Proxy randomOrCreate(array $attributes = []) + * @phpstan-method static ProxyRepositoryDecorator 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 +final class PostFactory extends PersistentProxyObjectFactory { /** * @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#factories-as-services @@ -61,7 +62,11 @@ final class PostFactory extends ModelFactory */ public function __construct() { - parent::__construct(); + } + + public static function class(): string + { + return Post::class; } /** @@ -69,7 +74,7 @@ public function __construct() * * @todo add your default values here */ - protected function getDefaults(): array + protected function defaults(): array|callable { return [ 'body' => self::faker()->text(), @@ -82,15 +87,10 @@ protected function getDefaults(): array /** * @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#initialization */ - protected function initialize(): self + protected function initialize(): static { return $this // ->afterInstantiate(function(Post $post): void {}) ; } - - protected static function getClass(): string - { - return Post::class; - } } diff --git a/tests/Fixtures/Maker/expected/can_create_factory_for_entity_with_repository_with_data_set_psalm.php b/tests/Fixtures/Maker/expected/can_create_factory_for_entity_with_repository_with_data_set_psalm.php index 8b1476353..260b59692 100644 --- a/tests/Fixtures/Maker/expected/can_create_factory_for_entity_with_repository_with_data_set_psalm.php +++ b/tests/Fixtures/Maker/expected/can_create_factory_for_entity_with_repository_with_data_set_psalm.php @@ -11,48 +11,49 @@ namespace App\Factory; -use Zenstruck\Foundry\ModelFactory; -use Zenstruck\Foundry\Proxy; -use Zenstruck\Foundry\RepositoryProxy; +use Doctrine\ORM\EntityRepository; +use Zenstruck\Foundry\Persistence\PersistentProxyObjectFactory; +use Zenstruck\Foundry\Persistence\Proxy; +use Zenstruck\Foundry\Persistence\ProxyRepositoryDecorator; use Zenstruck\Foundry\Tests\Fixtures\Entity\Post; use Zenstruck\Foundry\Tests\Fixtures\Repository\PostRepository; /** - * @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 = []) * - * @psalm-method Proxy create(array|callable $attributes = []) - * @psalm-method static Proxy createOne(array $attributes = []) - * @psalm-method static Proxy find(object|array|mixed $criteria) - * @psalm-method static Proxy findOrCreate(array $attributes) - * @psalm-method static Proxy first(string $sortedField = 'id') - * @psalm-method static Proxy last(string $sortedField = 'id') - * @psalm-method static Proxy random(array $attributes = []) - * @psalm-method static Proxy randomOrCreate(array $attributes = []) - * @psalm-method static RepositoryProxy repository() - * @psalm-method static list> all() - * @psalm-method static list> createMany(int $number, array|callable $attributes = []) - * @psalm-method static list> createSequence(iterable|callable $sequence) - * @psalm-method static list> findBy(array $attributes) - * @psalm-method static list> randomRange(int $min, int $max, array $attributes = []) - * @psalm-method static list> randomSet(int $number, array $attributes = []) + * @psalm-method Post&Proxy create(array|callable $attributes = []) + * @psalm-method static Post&Proxy createOne(array $attributes = []) + * @psalm-method static Post&Proxy find(object|array|mixed $criteria) + * @psalm-method static Post&Proxy findOrCreate(array $attributes) + * @psalm-method static Post&Proxy first(string $sortedField = 'id') + * @psalm-method static Post&Proxy last(string $sortedField = 'id') + * @psalm-method static Post&Proxy random(array $attributes = []) + * @psalm-method static Post&Proxy randomOrCreate(array $attributes = []) + * @psalm-method static ProxyRepositoryDecorator repository() + * @psalm-method static list> all() + * @psalm-method static list> createMany(int $number, array|callable $attributes = []) + * @psalm-method static list> createSequence(iterable|callable $sequence) + * @psalm-method static list> findBy(array $attributes) + * @psalm-method static list> randomRange(int $min, int $max, array $attributes = []) + * @psalm-method static list> randomSet(int $number, array $attributes = []) */ -final class PostFactory extends ModelFactory +final class PostFactory extends PersistentProxyObjectFactory { /** * @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#factories-as-services @@ -61,7 +62,11 @@ final class PostFactory extends ModelFactory */ public function __construct() { - parent::__construct(); + } + + public static function class(): string + { + return Post::class; } /** @@ -69,7 +74,7 @@ public function __construct() * * @todo add your default values here */ - protected function getDefaults(): array + protected function defaults(): array|callable { return [ 'body' => self::faker()->text(), @@ -82,15 +87,10 @@ protected function getDefaults(): array /** * @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#initialization */ - protected function initialize(): self + protected function initialize(): static { return $this // ->afterInstantiate(function(Post $post): void {}) ; } - - protected static function getClass(): string - { - return Post::class; - } } diff --git a/tests/Fixtures/Maker/expected/can_create_factory_for_not_persisted_class.php b/tests/Fixtures/Maker/expected/can_create_factory_for_not_persisted_class.php index f6fbf9a1c..060117148 100644 --- a/tests/Fixtures/Maker/expected/can_create_factory_for_not_persisted_class.php +++ b/tests/Fixtures/Maker/expected/can_create_factory_for_not_persisted_class.php @@ -11,19 +11,13 @@ namespace App\Factory; -use Zenstruck\Foundry\ModelFactory; -use Zenstruck\Foundry\Proxy; +use Zenstruck\Foundry\ObjectFactory; use Zenstruck\Foundry\Tests\Fixtures\Object\SomeObject; /** - * @extends ModelFactory - * - * @method SomeObject|Proxy create(array|callable $attributes = []) - * @method static SomeObject|Proxy createOne(array $attributes = []) - * @method static SomeObject[]|Proxy[] createMany(int $number, array|callable $attributes = []) - * @method static SomeObject[]|Proxy[] createSequence(iterable|callable $sequence) + * @extends ObjectFactory */ -final class SomeObjectFactory extends ModelFactory +final class SomeObjectFactory extends ObjectFactory { /** * @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#factories-as-services @@ -32,7 +26,11 @@ final class SomeObjectFactory extends ModelFactory */ public function __construct() { - parent::__construct(); + } + + public static function class(): string + { + return SomeObject::class; } /** @@ -40,7 +38,7 @@ public function __construct() * * @todo add your default values here */ - protected function getDefaults(): array + protected function defaults(): array|callable { return [ 'arrayMandatory' => [], @@ -60,16 +58,10 @@ protected function getDefaults(): array /** * @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#initialization */ - protected function initialize(): self + protected function initialize(): static { return $this - ->withoutPersisting() // ->afterInstantiate(function(SomeObject $someObject): void {}) ; } - - protected static function getClass(): string - { - return SomeObject::class; - } } diff --git a/tests/Fixtures/Maker/expected/can_create_factory_for_not_persisted_class_interactively.php b/tests/Fixtures/Maker/expected/can_create_factory_for_not_persisted_class_interactively.php index c9c68e114..0e97c289c 100644 --- a/tests/Fixtures/Maker/expected/can_create_factory_for_not_persisted_class_interactively.php +++ b/tests/Fixtures/Maker/expected/can_create_factory_for_not_persisted_class_interactively.php @@ -11,19 +11,13 @@ namespace App\Factory; -use Zenstruck\Foundry\ModelFactory; -use Zenstruck\Foundry\Proxy; +use Zenstruck\Foundry\ObjectFactory; use Zenstruck\Foundry\Tests\Fixtures\Object\SomeObject; /** - * @extends ModelFactory - * - * @method SomeObject|Proxy create(array|callable $attributes = []) - * @method static SomeObject|Proxy createOne(array $attributes = []) - * @method static SomeObject[]|Proxy[] createMany(int $number, array|callable $attributes = []) - * @method static SomeObject[]|Proxy[] createSequence(iterable|callable $sequence) + * @extends ObjectFactory */ -final class SomeObjectFactory extends ModelFactory +final class SomeObjectFactory extends ObjectFactory { /** * @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#factories-as-services @@ -32,7 +26,11 @@ final class SomeObjectFactory extends ModelFactory */ public function __construct() { - parent::__construct(); + } + + public static function class(): string + { + return SomeObject::class; } /** @@ -40,7 +38,7 @@ public function __construct() * * @todo add your default values here */ - protected function getDefaults(): array + protected function defaults(): array|callable { return [ 'arrayMandatory' => [], @@ -58,16 +56,10 @@ protected function getDefaults(): array /** * @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#initialization */ - protected function initialize(): self + protected function initialize(): static { return $this - ->withoutPersisting() // ->afterInstantiate(function(SomeObject $someObject): void {}) ; } - - protected static function getClass(): string - { - return SomeObject::class; - } } diff --git a/tests/Fixtures/Maker/expected/can_create_factory_for_odm_with_data_set_document.php b/tests/Fixtures/Maker/expected/can_create_factory_for_odm_with_data_set_document.php index 79de12d12..03c08193d 100644 --- a/tests/Fixtures/Maker/expected/can_create_factory_for_odm_with_data_set_document.php +++ b/tests/Fixtures/Maker/expected/can_create_factory_for_odm_with_data_set_document.php @@ -12,31 +12,31 @@ namespace App\Factory; use Doctrine\ODM\MongoDB\Repository\DocumentRepository; -use Zenstruck\Foundry\ModelFactory; -use Zenstruck\Foundry\Proxy; -use Zenstruck\Foundry\RepositoryProxy; +use Zenstruck\Foundry\Persistence\PersistentProxyObjectFactory; +use Zenstruck\Foundry\Persistence\Proxy; +use Zenstruck\Foundry\Persistence\ProxyRepositoryDecorator; use Zenstruck\Foundry\Tests\Fixtures\Document\ODMPost; /** - * @extends ModelFactory + * @extends PersistentProxyObjectFactory * - * @method ODMPost|Proxy create(array|callable $attributes = []) - * @method static ODMPost|Proxy createOne(array $attributes = []) - * @method static ODMPost|Proxy find(object|array|mixed $criteria) - * @method static ODMPost|Proxy findOrCreate(array $attributes) - * @method static ODMPost|Proxy first(string $sortedField = 'id') - * @method static ODMPost|Proxy last(string $sortedField = 'id') - * @method static ODMPost|Proxy random(array $attributes = []) - * @method static ODMPost|Proxy randomOrCreate(array $attributes = []) - * @method static DocumentRepository|RepositoryProxy repository() - * @method static ODMPost[]|Proxy[] all() - * @method static ODMPost[]|Proxy[] createMany(int $number, array|callable $attributes = []) - * @method static ODMPost[]|Proxy[] createSequence(iterable|callable $sequence) - * @method static ODMPost[]|Proxy[] findBy(array $attributes) - * @method static ODMPost[]|Proxy[] randomRange(int $min, int $max, array $attributes = []) - * @method static ODMPost[]|Proxy[] randomSet(int $number, array $attributes = []) + * @method ODMPost|Proxy create(array|callable $attributes = []) + * @method static ODMPost|Proxy createOne(array $attributes = []) + * @method static ODMPost|Proxy find(object|array|mixed $criteria) + * @method static ODMPost|Proxy findOrCreate(array $attributes) + * @method static ODMPost|Proxy first(string $sortedField = 'id') + * @method static ODMPost|Proxy last(string $sortedField = 'id') + * @method static ODMPost|Proxy random(array $attributes = []) + * @method static ODMPost|Proxy randomOrCreate(array $attributes = []) + * @method static DocumentRepository|ProxyRepositoryDecorator repository() + * @method static ODMPost[]|Proxy[] all() + * @method static ODMPost[]|Proxy[] createMany(int $number, array|callable $attributes = []) + * @method static ODMPost[]|Proxy[] createSequence(iterable|callable $sequence) + * @method static ODMPost[]|Proxy[] findBy(array $attributes) + * @method static ODMPost[]|Proxy[] randomRange(int $min, int $max, array $attributes = []) + * @method static ODMPost[]|Proxy[] randomSet(int $number, array $attributes = []) */ -final class ODMPostFactory extends ModelFactory +final class ODMPostFactory extends PersistentProxyObjectFactory { /** * @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#factories-as-services @@ -45,7 +45,11 @@ final class ODMPostFactory extends ModelFactory */ public function __construct() { - parent::__construct(); + } + + public static function class(): string + { + return ODMPost::class; } /** @@ -53,7 +57,7 @@ public function __construct() * * @todo add your default values here */ - protected function getDefaults(): array + protected function defaults(): array|callable { return [ 'body' => self::faker()->text(), @@ -66,15 +70,10 @@ protected function getDefaults(): array /** * @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#initialization */ - protected function initialize(): self + protected function initialize(): static { return $this // ->afterInstantiate(function(ODMPost $oDMPost): void {}) ; } - - protected static function getClass(): string - { - return ODMPost::class; - } } diff --git a/tests/Fixtures/Maker/expected/can_create_factory_for_odm_with_data_set_embedded_document.php b/tests/Fixtures/Maker/expected/can_create_factory_for_odm_with_data_set_embedded_document.php index 9b4c7526b..9002624ca 100644 --- a/tests/Fixtures/Maker/expected/can_create_factory_for_odm_with_data_set_embedded_document.php +++ b/tests/Fixtures/Maker/expected/can_create_factory_for_odm_with_data_set_embedded_document.php @@ -11,20 +11,14 @@ namespace App\Factory; -use Zenstruck\Foundry\ModelFactory; -use Zenstruck\Foundry\Proxy; +use Zenstruck\Foundry\ObjectFactory; use Zenstruck\Foundry\Tests\Fixtures\Document\ODMComment; use Zenstruck\Foundry\Tests\Fixtures\Factories\ODM\UserFactory; /** - * @extends ModelFactory - * - * @method ODMComment|Proxy create(array|callable $attributes = []) - * @method static ODMComment|Proxy createOne(array $attributes = []) - * @method static ODMComment[]|Proxy[] createMany(int $number, array|callable $attributes = []) - * @method static ODMComment[]|Proxy[] createSequence(iterable|callable $sequence) + * @extends ObjectFactory */ -final class ODMCommentFactory extends ModelFactory +final class ODMCommentFactory extends ObjectFactory { /** * @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#factories-as-services @@ -33,7 +27,11 @@ final class ODMCommentFactory extends ModelFactory */ public function __construct() { - parent::__construct(); + } + + public static function class(): string + { + return ODMComment::class; } /** @@ -41,7 +39,7 @@ public function __construct() * * @todo add your default values here */ - protected function getDefaults(): array + protected function defaults(): array|callable { return [ 'body' => self::faker()->sentence(), @@ -52,16 +50,10 @@ protected function getDefaults(): array /** * @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#initialization */ - protected function initialize(): self + protected function initialize(): static { return $this - ->withoutPersisting() // ->afterInstantiate(function(ODMComment $oDMComment): void {}) ; } - - protected static function getClass(): string - { - return ODMComment::class; - } } diff --git a/tests/Fixtures/Maker/expected/can_create_factory_for_orm_embedded_class.php b/tests/Fixtures/Maker/expected/can_create_factory_for_orm_embedded_class.php index 082dfdaa2..8f65c4b3c 100644 --- a/tests/Fixtures/Maker/expected/can_create_factory_for_orm_embedded_class.php +++ b/tests/Fixtures/Maker/expected/can_create_factory_for_orm_embedded_class.php @@ -11,19 +11,13 @@ namespace App\Factory; -use Zenstruck\Foundry\ModelFactory; -use Zenstruck\Foundry\Proxy; +use Zenstruck\Foundry\ObjectFactory; use Zenstruck\Foundry\Tests\Fixtures\Entity\Address; /** - * @extends ModelFactory
- * - * @method Address|Proxy create(array|callable $attributes = []) - * @method static Address|Proxy createOne(array $attributes = []) - * @method static Address[]|Proxy[] createMany(int $number, array|callable $attributes = []) - * @method static Address[]|Proxy[] createSequence(iterable|callable $sequence) + * @extends ObjectFactory
*/ -final class AddressFactory extends ModelFactory +final class AddressFactory extends ObjectFactory { /** * @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#factories-as-services @@ -32,7 +26,11 @@ final class AddressFactory extends ModelFactory */ public function __construct() { - parent::__construct(); + } + + public static function class(): string + { + return Address::class; } /** @@ -40,7 +38,7 @@ public function __construct() * * @todo add your default values here */ - protected function getDefaults(): array + protected function defaults(): array|callable { return [ 'value' => self::faker()->sentence(), @@ -50,16 +48,10 @@ protected function getDefaults(): array /** * @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#initialization */ - protected function initialize(): self + protected function initialize(): static { return $this - ->withoutPersisting() // ->afterInstantiate(function(Address $address): void {}) ; } - - protected static function getClass(): string - { - return Address::class; - } } diff --git a/tests/Fixtures/Maker/expected/can_create_factory_in_test_dir.php b/tests/Fixtures/Maker/expected/can_create_factory_in_test_dir.php index d614ec60a..a814f9e96 100644 --- a/tests/Fixtures/Maker/expected/can_create_factory_in_test_dir.php +++ b/tests/Fixtures/Maker/expected/can_create_factory_in_test_dir.php @@ -12,31 +12,31 @@ namespace App\Tests\Factory; use Doctrine\ORM\EntityRepository; -use Zenstruck\Foundry\ModelFactory; -use Zenstruck\Foundry\Proxy; -use Zenstruck\Foundry\RepositoryProxy; +use Zenstruck\Foundry\Persistence\PersistentProxyObjectFactory; +use Zenstruck\Foundry\Persistence\Proxy; +use Zenstruck\Foundry\Persistence\ProxyRepositoryDecorator; use Zenstruck\Foundry\Tests\Fixtures\Entity\Category; /** - * @extends ModelFactory + * @extends PersistentProxyObjectFactory * - * @method Category|Proxy create(array|callable $attributes = []) - * @method static Category|Proxy createOne(array $attributes = []) - * @method static Category|Proxy find(object|array|mixed $criteria) - * @method static Category|Proxy findOrCreate(array $attributes) - * @method static Category|Proxy first(string $sortedField = 'id') - * @method static Category|Proxy last(string $sortedField = 'id') - * @method static Category|Proxy random(array $attributes = []) - * @method static Category|Proxy randomOrCreate(array $attributes = []) - * @method static EntityRepository|RepositoryProxy repository() - * @method static Category[]|Proxy[] all() - * @method static Category[]|Proxy[] createMany(int $number, array|callable $attributes = []) - * @method static Category[]|Proxy[] createSequence(iterable|callable $sequence) - * @method static Category[]|Proxy[] findBy(array $attributes) - * @method static Category[]|Proxy[] randomRange(int $min, int $max, array $attributes = []) - * @method static Category[]|Proxy[] randomSet(int $number, array $attributes = []) + * @method Category|Proxy create(array|callable $attributes = []) + * @method static Category|Proxy createOne(array $attributes = []) + * @method static Category|Proxy find(object|array|mixed $criteria) + * @method static Category|Proxy findOrCreate(array $attributes) + * @method static Category|Proxy first(string $sortedField = 'id') + * @method static Category|Proxy last(string $sortedField = 'id') + * @method static Category|Proxy random(array $attributes = []) + * @method static Category|Proxy randomOrCreate(array $attributes = []) + * @method static EntityRepository|ProxyRepositoryDecorator repository() + * @method static Category[]|Proxy[] all() + * @method static Category[]|Proxy[] createMany(int $number, array|callable $attributes = []) + * @method static Category[]|Proxy[] createSequence(iterable|callable $sequence) + * @method static Category[]|Proxy[] findBy(array $attributes) + * @method static Category[]|Proxy[] randomRange(int $min, int $max, array $attributes = []) + * @method static Category[]|Proxy[] randomSet(int $number, array $attributes = []) */ -final class CategoryFactory extends ModelFactory +final class CategoryFactory extends PersistentProxyObjectFactory { /** * @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#factories-as-services @@ -45,7 +45,11 @@ final class CategoryFactory extends ModelFactory */ public function __construct() { - parent::__construct(); + } + + public static function class(): string + { + return Category::class; } /** @@ -53,7 +57,7 @@ public function __construct() * * @todo add your default values here */ - protected function getDefaults(): array + protected function defaults(): array|callable { return [ 'name' => self::faker()->text(255), @@ -63,15 +67,10 @@ protected function getDefaults(): array /** * @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#initialization */ - protected function initialize(): self + protected function initialize(): static { return $this // ->afterInstantiate(function(Category $category): void {}) ; } - - protected static function getClass(): string - { - return Category::class; - } } diff --git a/tests/Fixtures/Maker/expected/can_create_factory_in_test_dir_interactively.php b/tests/Fixtures/Maker/expected/can_create_factory_in_test_dir_interactively.php index 9f8343ffd..8749ad400 100644 --- a/tests/Fixtures/Maker/expected/can_create_factory_in_test_dir_interactively.php +++ b/tests/Fixtures/Maker/expected/can_create_factory_in_test_dir_interactively.php @@ -12,31 +12,31 @@ namespace App\Tests\Factory; use Doctrine\ORM\EntityRepository; -use Zenstruck\Foundry\ModelFactory; -use Zenstruck\Foundry\Proxy; -use Zenstruck\Foundry\RepositoryProxy; +use Zenstruck\Foundry\Persistence\PersistentProxyObjectFactory; +use Zenstruck\Foundry\Persistence\Proxy; +use Zenstruck\Foundry\Persistence\ProxyRepositoryDecorator; use Zenstruck\Foundry\Tests\Fixtures\Entity\Tag; /** - * @extends ModelFactory + * @extends PersistentProxyObjectFactory * - * @method Tag|Proxy create(array|callable $attributes = []) - * @method static Tag|Proxy createOne(array $attributes = []) - * @method static Tag|Proxy find(object|array|mixed $criteria) - * @method static Tag|Proxy findOrCreate(array $attributes) - * @method static Tag|Proxy first(string $sortedField = 'id') - * @method static Tag|Proxy last(string $sortedField = 'id') - * @method static Tag|Proxy random(array $attributes = []) - * @method static Tag|Proxy randomOrCreate(array $attributes = []) - * @method static EntityRepository|RepositoryProxy repository() - * @method static Tag[]|Proxy[] all() - * @method static Tag[]|Proxy[] createMany(int $number, array|callable $attributes = []) - * @method static Tag[]|Proxy[] createSequence(iterable|callable $sequence) - * @method static Tag[]|Proxy[] findBy(array $attributes) - * @method static Tag[]|Proxy[] randomRange(int $min, int $max, array $attributes = []) - * @method static Tag[]|Proxy[] randomSet(int $number, array $attributes = []) + * @method Tag|Proxy create(array|callable $attributes = []) + * @method static Tag|Proxy createOne(array $attributes = []) + * @method static Tag|Proxy find(object|array|mixed $criteria) + * @method static Tag|Proxy findOrCreate(array $attributes) + * @method static Tag|Proxy first(string $sortedField = 'id') + * @method static Tag|Proxy last(string $sortedField = 'id') + * @method static Tag|Proxy random(array $attributes = []) + * @method static Tag|Proxy randomOrCreate(array $attributes = []) + * @method static EntityRepository|ProxyRepositoryDecorator repository() + * @method static Tag[]|Proxy[] all() + * @method static Tag[]|Proxy[] createMany(int $number, array|callable $attributes = []) + * @method static Tag[]|Proxy[] createSequence(iterable|callable $sequence) + * @method static Tag[]|Proxy[] findBy(array $attributes) + * @method static Tag[]|Proxy[] randomRange(int $min, int $max, array $attributes = []) + * @method static Tag[]|Proxy[] randomSet(int $number, array $attributes = []) */ -final class TagFactory extends ModelFactory +final class TagFactory extends PersistentProxyObjectFactory { /** * @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#factories-as-services @@ -45,7 +45,11 @@ final class TagFactory extends ModelFactory */ public function __construct() { - parent::__construct(); + } + + public static function class(): string + { + return Tag::class; } /** @@ -53,7 +57,7 @@ public function __construct() * * @todo add your default values here */ - protected function getDefaults(): array + protected function defaults(): array|callable { return [ 'name' => self::faker()->text(255), @@ -63,15 +67,10 @@ protected function getDefaults(): array /** * @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#initialization */ - protected function initialize(): self + protected function initialize(): static { return $this // ->afterInstantiate(function(Tag $tag): void {}) ; } - - protected static function getClass(): string - { - return Tag::class; - } } diff --git a/tests/Fixtures/Maker/expected/can_create_factory_interactively.php b/tests/Fixtures/Maker/expected/can_create_factory_interactively.php index 6065c4c8b..925ba945f 100644 --- a/tests/Fixtures/Maker/expected/can_create_factory_interactively.php +++ b/tests/Fixtures/Maker/expected/can_create_factory_interactively.php @@ -12,31 +12,31 @@ namespace App\Factory; use Doctrine\ORM\EntityRepository; -use Zenstruck\Foundry\ModelFactory; -use Zenstruck\Foundry\Proxy; -use Zenstruck\Foundry\RepositoryProxy; +use Zenstruck\Foundry\Persistence\PersistentProxyObjectFactory; +use Zenstruck\Foundry\Persistence\Proxy; +use Zenstruck\Foundry\Persistence\ProxyRepositoryDecorator; use Zenstruck\Foundry\Tests\Fixtures\Entity\Comment; /** - * @extends ModelFactory + * @extends PersistentProxyObjectFactory * - * @method Comment|Proxy create(array|callable $attributes = []) - * @method static Comment|Proxy createOne(array $attributes = []) - * @method static Comment|Proxy find(object|array|mixed $criteria) - * @method static Comment|Proxy findOrCreate(array $attributes) - * @method static Comment|Proxy first(string $sortedField = 'id') - * @method static Comment|Proxy last(string $sortedField = 'id') - * @method static Comment|Proxy random(array $attributes = []) - * @method static Comment|Proxy randomOrCreate(array $attributes = []) - * @method static EntityRepository|RepositoryProxy repository() - * @method static Comment[]|Proxy[] all() - * @method static Comment[]|Proxy[] createMany(int $number, array|callable $attributes = []) - * @method static Comment[]|Proxy[] createSequence(iterable|callable $sequence) - * @method static Comment[]|Proxy[] findBy(array $attributes) - * @method static Comment[]|Proxy[] randomRange(int $min, int $max, array $attributes = []) - * @method static Comment[]|Proxy[] randomSet(int $number, array $attributes = []) + * @method Comment|Proxy create(array|callable $attributes = []) + * @method static Comment|Proxy createOne(array $attributes = []) + * @method static Comment|Proxy find(object|array|mixed $criteria) + * @method static Comment|Proxy findOrCreate(array $attributes) + * @method static Comment|Proxy first(string $sortedField = 'id') + * @method static Comment|Proxy last(string $sortedField = 'id') + * @method static Comment|Proxy random(array $attributes = []) + * @method static Comment|Proxy randomOrCreate(array $attributes = []) + * @method static EntityRepository|ProxyRepositoryDecorator repository() + * @method static Comment[]|Proxy[] all() + * @method static Comment[]|Proxy[] createMany(int $number, array|callable $attributes = []) + * @method static Comment[]|Proxy[] createSequence(iterable|callable $sequence) + * @method static Comment[]|Proxy[] findBy(array $attributes) + * @method static Comment[]|Proxy[] randomRange(int $min, int $max, array $attributes = []) + * @method static Comment[]|Proxy[] randomSet(int $number, array $attributes = []) */ -final class CommentFactory extends ModelFactory +final class CommentFactory extends PersistentProxyObjectFactory { /** * @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#factories-as-services @@ -45,7 +45,11 @@ final class CommentFactory extends ModelFactory */ public function __construct() { - parent::__construct(); + } + + public static function class(): string + { + return Comment::class; } /** @@ -53,7 +57,7 @@ public function __construct() * * @todo add your default values here */ - protected function getDefaults(): array + protected function defaults(): array|callable { return [ 'approved' => self::faker()->boolean(), @@ -67,15 +71,10 @@ protected function getDefaults(): array /** * @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#initialization */ - protected function initialize(): self + protected function initialize(): static { return $this // ->afterInstantiate(function(Comment $comment): void {}) ; } - - protected static function getClass(): string - { - return Comment::class; - } } diff --git a/tests/Fixtures/Maker/expected/can_create_factory_with_auto_activated_not_persisted_option.php b/tests/Fixtures/Maker/expected/can_create_factory_with_auto_activated_not_persisted_option.php index 25525c296..594dd4b2e 100644 --- a/tests/Fixtures/Maker/expected/can_create_factory_with_auto_activated_not_persisted_option.php +++ b/tests/Fixtures/Maker/expected/can_create_factory_with_auto_activated_not_persisted_option.php @@ -11,19 +11,13 @@ namespace App\Factory; -use Zenstruck\Foundry\ModelFactory; -use Zenstruck\Foundry\Proxy; +use Zenstruck\Foundry\ObjectFactory; use Zenstruck\Foundry\Tests\Fixtures\Entity\Category; /** - * @extends ModelFactory - * - * @method Category|Proxy create(array|callable $attributes = []) - * @method static Category|Proxy createOne(array $attributes = []) - * @method static Category[]|Proxy[] createMany(int $number, array|callable $attributes = []) - * @method static Category[]|Proxy[] createSequence(iterable|callable $sequence) + * @extends ObjectFactory */ -final class CategoryFactory extends ModelFactory +final class CategoryFactory extends ObjectFactory { /** * @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#factories-as-services @@ -32,7 +26,11 @@ final class CategoryFactory extends ModelFactory */ public function __construct() { - parent::__construct(); + } + + public static function class(): string + { + return Category::class; } /** @@ -40,7 +38,7 @@ public function __construct() * * @todo add your default values here */ - protected function getDefaults(): array + protected function defaults(): array|callable { return [ 'posts' => null, // TODO add Doctrine\Common\Collections\Collection value manually @@ -51,16 +49,10 @@ protected function getDefaults(): array /** * @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#initialization */ - protected function initialize(): self + protected function initialize(): static { return $this - ->withoutPersisting() // ->afterInstantiate(function(Category $category): void {}) ; } - - protected static function getClass(): string - { - return Category::class; - } } diff --git a/tests/Fixtures/Maker/expected/can_create_factory_with_default_enum_with_data_set_odm.php b/tests/Fixtures/Maker/expected/can_create_factory_with_default_enum_with_data_set_odm.php index d1be7fd79..b63b44ad8 100644 --- a/tests/Fixtures/Maker/expected/can_create_factory_with_default_enum_with_data_set_odm.php +++ b/tests/Fixtures/Maker/expected/can_create_factory_with_default_enum_with_data_set_odm.php @@ -12,32 +12,32 @@ namespace App\Factory; use Doctrine\ODM\MongoDB\Repository\DocumentRepository; -use Zenstruck\Foundry\ModelFactory; -use Zenstruck\Foundry\Proxy; -use Zenstruck\Foundry\RepositoryProxy; +use Zenstruck\Foundry\Persistence\PersistentProxyObjectFactory; +use Zenstruck\Foundry\Persistence\Proxy; +use Zenstruck\Foundry\Persistence\ProxyRepositoryDecorator; use Zenstruck\Foundry\Tests\Fixtures\PHP81\DocumentWithEnum; use Zenstruck\Foundry\Tests\Fixtures\PHP81\SomeEnum; /** - * @extends ModelFactory + * @extends PersistentProxyObjectFactory * - * @method DocumentWithEnum|Proxy create(array|callable $attributes = []) - * @method static DocumentWithEnum|Proxy createOne(array $attributes = []) - * @method static DocumentWithEnum|Proxy find(object|array|mixed $criteria) - * @method static DocumentWithEnum|Proxy findOrCreate(array $attributes) - * @method static DocumentWithEnum|Proxy first(string $sortedField = 'id') - * @method static DocumentWithEnum|Proxy last(string $sortedField = 'id') - * @method static DocumentWithEnum|Proxy random(array $attributes = []) - * @method static DocumentWithEnum|Proxy randomOrCreate(array $attributes = []) - * @method static DocumentRepository|RepositoryProxy repository() - * @method static DocumentWithEnum[]|Proxy[] all() - * @method static DocumentWithEnum[]|Proxy[] createMany(int $number, array|callable $attributes = []) - * @method static DocumentWithEnum[]|Proxy[] createSequence(iterable|callable $sequence) - * @method static DocumentWithEnum[]|Proxy[] findBy(array $attributes) - * @method static DocumentWithEnum[]|Proxy[] randomRange(int $min, int $max, array $attributes = []) - * @method static DocumentWithEnum[]|Proxy[] randomSet(int $number, array $attributes = []) + * @method DocumentWithEnum|Proxy create(array|callable $attributes = []) + * @method static DocumentWithEnum|Proxy createOne(array $attributes = []) + * @method static DocumentWithEnum|Proxy find(object|array|mixed $criteria) + * @method static DocumentWithEnum|Proxy findOrCreate(array $attributes) + * @method static DocumentWithEnum|Proxy first(string $sortedField = 'id') + * @method static DocumentWithEnum|Proxy last(string $sortedField = 'id') + * @method static DocumentWithEnum|Proxy random(array $attributes = []) + * @method static DocumentWithEnum|Proxy randomOrCreate(array $attributes = []) + * @method static DocumentRepository|ProxyRepositoryDecorator repository() + * @method static DocumentWithEnum[]|Proxy[] all() + * @method static DocumentWithEnum[]|Proxy[] createMany(int $number, array|callable $attributes = []) + * @method static DocumentWithEnum[]|Proxy[] createSequence(iterable|callable $sequence) + * @method static DocumentWithEnum[]|Proxy[] findBy(array $attributes) + * @method static DocumentWithEnum[]|Proxy[] randomRange(int $min, int $max, array $attributes = []) + * @method static DocumentWithEnum[]|Proxy[] randomSet(int $number, array $attributes = []) */ -final class DocumentWithEnumFactory extends ModelFactory +final class DocumentWithEnumFactory extends PersistentProxyObjectFactory { /** * @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#factories-as-services @@ -46,7 +46,11 @@ final class DocumentWithEnumFactory extends ModelFactory */ public function __construct() { - parent::__construct(); + } + + public static function class(): string + { + return DocumentWithEnum::class; } /** @@ -54,7 +58,7 @@ public function __construct() * * @todo add your default values here */ - protected function getDefaults(): array + protected function defaults(): array|callable { return [ 'enum' => self::faker()->randomElement(SomeEnum::cases()), @@ -64,15 +68,10 @@ protected function getDefaults(): array /** * @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#initialization */ - protected function initialize(): self + protected function initialize(): static { return $this // ->afterInstantiate(function(DocumentWithEnum $documentWithEnum): void {}) ; } - - protected static function getClass(): string - { - return DocumentWithEnum::class; - } } diff --git a/tests/Fixtures/Maker/expected/can_create_factory_with_default_enum_with_data_set_orm.php b/tests/Fixtures/Maker/expected/can_create_factory_with_default_enum_with_data_set_orm.php index 1a0f41032..aafca82a6 100644 --- a/tests/Fixtures/Maker/expected/can_create_factory_with_default_enum_with_data_set_orm.php +++ b/tests/Fixtures/Maker/expected/can_create_factory_with_default_enum_with_data_set_orm.php @@ -12,32 +12,32 @@ namespace App\Factory; use Doctrine\ORM\EntityRepository; -use Zenstruck\Foundry\ModelFactory; -use Zenstruck\Foundry\Proxy; -use Zenstruck\Foundry\RepositoryProxy; +use Zenstruck\Foundry\Persistence\PersistentProxyObjectFactory; +use Zenstruck\Foundry\Persistence\Proxy; +use Zenstruck\Foundry\Persistence\ProxyRepositoryDecorator; use Zenstruck\Foundry\Tests\Fixtures\PHP81\EntityWithEnum; use Zenstruck\Foundry\Tests\Fixtures\PHP81\SomeEnum; /** - * @extends ModelFactory + * @extends PersistentProxyObjectFactory * - * @method EntityWithEnum|Proxy create(array|callable $attributes = []) - * @method static EntityWithEnum|Proxy createOne(array $attributes = []) - * @method static EntityWithEnum|Proxy find(object|array|mixed $criteria) - * @method static EntityWithEnum|Proxy findOrCreate(array $attributes) - * @method static EntityWithEnum|Proxy first(string $sortedField = 'id') - * @method static EntityWithEnum|Proxy last(string $sortedField = 'id') - * @method static EntityWithEnum|Proxy random(array $attributes = []) - * @method static EntityWithEnum|Proxy randomOrCreate(array $attributes = []) - * @method static EntityRepository|RepositoryProxy repository() - * @method static EntityWithEnum[]|Proxy[] all() - * @method static EntityWithEnum[]|Proxy[] createMany(int $number, array|callable $attributes = []) - * @method static EntityWithEnum[]|Proxy[] createSequence(iterable|callable $sequence) - * @method static EntityWithEnum[]|Proxy[] findBy(array $attributes) - * @method static EntityWithEnum[]|Proxy[] randomRange(int $min, int $max, array $attributes = []) - * @method static EntityWithEnum[]|Proxy[] randomSet(int $number, array $attributes = []) + * @method EntityWithEnum|Proxy create(array|callable $attributes = []) + * @method static EntityWithEnum|Proxy createOne(array $attributes = []) + * @method static EntityWithEnum|Proxy find(object|array|mixed $criteria) + * @method static EntityWithEnum|Proxy findOrCreate(array $attributes) + * @method static EntityWithEnum|Proxy first(string $sortedField = 'id') + * @method static EntityWithEnum|Proxy last(string $sortedField = 'id') + * @method static EntityWithEnum|Proxy random(array $attributes = []) + * @method static EntityWithEnum|Proxy randomOrCreate(array $attributes = []) + * @method static EntityRepository|ProxyRepositoryDecorator repository() + * @method static EntityWithEnum[]|Proxy[] all() + * @method static EntityWithEnum[]|Proxy[] createMany(int $number, array|callable $attributes = []) + * @method static EntityWithEnum[]|Proxy[] createSequence(iterable|callable $sequence) + * @method static EntityWithEnum[]|Proxy[] findBy(array $attributes) + * @method static EntityWithEnum[]|Proxy[] randomRange(int $min, int $max, array $attributes = []) + * @method static EntityWithEnum[]|Proxy[] randomSet(int $number, array $attributes = []) */ -final class EntityWithEnumFactory extends ModelFactory +final class EntityWithEnumFactory extends PersistentProxyObjectFactory { /** * @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#factories-as-services @@ -46,7 +46,11 @@ final class EntityWithEnumFactory extends ModelFactory */ public function __construct() { - parent::__construct(); + } + + public static function class(): string + { + return EntityWithEnum::class; } /** @@ -54,7 +58,7 @@ public function __construct() * * @todo add your default values here */ - protected function getDefaults(): array + protected function defaults(): array|callable { return [ 'enum' => self::faker()->randomElement(SomeEnum::cases()), @@ -64,15 +68,10 @@ protected function getDefaults(): array /** * @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#initialization */ - protected function initialize(): self + protected function initialize(): static { return $this // ->afterInstantiate(function(EntityWithEnum $entityWithEnum): void {}) ; } - - protected static function getClass(): string - { - return EntityWithEnum::class; - } } diff --git a/tests/Fixtures/Maker/expected/can_create_factory_with_default_enum_with_data_set_without_persistence.php b/tests/Fixtures/Maker/expected/can_create_factory_with_default_enum_with_data_set_without_persistence.php index adf6decea..c98dd5ef6 100644 --- a/tests/Fixtures/Maker/expected/can_create_factory_with_default_enum_with_data_set_without_persistence.php +++ b/tests/Fixtures/Maker/expected/can_create_factory_with_default_enum_with_data_set_without_persistence.php @@ -11,20 +11,14 @@ namespace App\Factory; -use Zenstruck\Foundry\ModelFactory; -use Zenstruck\Foundry\Proxy; +use Zenstruck\Foundry\ObjectFactory; use Zenstruck\Foundry\Tests\Fixtures\PHP81\EntityWithEnum; use Zenstruck\Foundry\Tests\Fixtures\PHP81\SomeEnum; /** - * @extends ModelFactory - * - * @method EntityWithEnum|Proxy create(array|callable $attributes = []) - * @method static EntityWithEnum|Proxy createOne(array $attributes = []) - * @method static EntityWithEnum[]|Proxy[] createMany(int $number, array|callable $attributes = []) - * @method static EntityWithEnum[]|Proxy[] createSequence(iterable|callable $sequence) + * @extends ObjectFactory */ -final class EntityWithEnumFactory extends ModelFactory +final class EntityWithEnumFactory extends ObjectFactory { /** * @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#factories-as-services @@ -33,7 +27,11 @@ final class EntityWithEnumFactory extends ModelFactory */ public function __construct() { - parent::__construct(); + } + + public static function class(): string + { + return EntityWithEnum::class; } /** @@ -41,7 +39,7 @@ public function __construct() * * @todo add your default values here */ - protected function getDefaults(): array + protected function defaults(): array|callable { return [ 'enum' => self::faker()->randomElement(SomeEnum::cases()), @@ -51,16 +49,10 @@ protected function getDefaults(): array /** * @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#initialization */ - protected function initialize(): self + protected function initialize(): static { return $this - ->withoutPersisting() // ->afterInstantiate(function(EntityWithEnum $entityWithEnum): void {}) ; } - - protected static function getClass(): string - { - return EntityWithEnum::class; - } } diff --git a/tests/Fixtures/Maker/expected/can_create_factory_with_embeddable_with_data_set_odm.php b/tests/Fixtures/Maker/expected/can_create_factory_with_embeddable_with_data_set_odm.php index 26a22f6cc..d8b243347 100644 --- a/tests/Fixtures/Maker/expected/can_create_factory_with_embeddable_with_data_set_odm.php +++ b/tests/Fixtures/Maker/expected/can_create_factory_with_embeddable_with_data_set_odm.php @@ -12,31 +12,31 @@ namespace App\Factory; use Doctrine\ODM\MongoDB\Repository\DocumentRepository; -use Zenstruck\Foundry\ModelFactory; -use Zenstruck\Foundry\Proxy; -use Zenstruck\Foundry\RepositoryProxy; +use Zenstruck\Foundry\Persistence\PersistentProxyObjectFactory; +use Zenstruck\Foundry\Persistence\Proxy; +use Zenstruck\Foundry\Persistence\ProxyRepositoryDecorator; use Zenstruck\Foundry\Tests\Fixtures\Document\ODMPost; /** - * @extends ModelFactory + * @extends PersistentProxyObjectFactory * - * @method ODMPost|Proxy create(array|callable $attributes = []) - * @method static ODMPost|Proxy createOne(array $attributes = []) - * @method static ODMPost|Proxy find(object|array|mixed $criteria) - * @method static ODMPost|Proxy findOrCreate(array $attributes) - * @method static ODMPost|Proxy first(string $sortedField = 'id') - * @method static ODMPost|Proxy last(string $sortedField = 'id') - * @method static ODMPost|Proxy random(array $attributes = []) - * @method static ODMPost|Proxy randomOrCreate(array $attributes = []) - * @method static DocumentRepository|RepositoryProxy repository() - * @method static ODMPost[]|Proxy[] all() - * @method static ODMPost[]|Proxy[] createMany(int $number, array|callable $attributes = []) - * @method static ODMPost[]|Proxy[] createSequence(iterable|callable $sequence) - * @method static ODMPost[]|Proxy[] findBy(array $attributes) - * @method static ODMPost[]|Proxy[] randomRange(int $min, int $max, array $attributes = []) - * @method static ODMPost[]|Proxy[] randomSet(int $number, array $attributes = []) + * @method ODMPost|Proxy create(array|callable $attributes = []) + * @method static ODMPost|Proxy createOne(array $attributes = []) + * @method static ODMPost|Proxy find(object|array|mixed $criteria) + * @method static ODMPost|Proxy findOrCreate(array $attributes) + * @method static ODMPost|Proxy first(string $sortedField = 'id') + * @method static ODMPost|Proxy last(string $sortedField = 'id') + * @method static ODMPost|Proxy random(array $attributes = []) + * @method static ODMPost|Proxy randomOrCreate(array $attributes = []) + * @method static DocumentRepository|ProxyRepositoryDecorator repository() + * @method static ODMPost[]|Proxy[] all() + * @method static ODMPost[]|Proxy[] createMany(int $number, array|callable $attributes = []) + * @method static ODMPost[]|Proxy[] createSequence(iterable|callable $sequence) + * @method static ODMPost[]|Proxy[] findBy(array $attributes) + * @method static ODMPost[]|Proxy[] randomRange(int $min, int $max, array $attributes = []) + * @method static ODMPost[]|Proxy[] randomSet(int $number, array $attributes = []) */ -final class ODMPostFactory extends ModelFactory +final class ODMPostFactory extends PersistentProxyObjectFactory { /** * @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#factories-as-services @@ -45,7 +45,11 @@ final class ODMPostFactory extends ModelFactory */ public function __construct() { - parent::__construct(); + } + + public static function class(): string + { + return ODMPost::class; } /** @@ -53,7 +57,7 @@ public function __construct() * * @todo add your default values here */ - protected function getDefaults(): array + protected function defaults(): array|callable { return [ 'body' => self::faker()->text(), @@ -69,15 +73,10 @@ protected function getDefaults(): array /** * @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#initialization */ - protected function initialize(): self + protected function initialize(): static { return $this // ->afterInstantiate(function(ODMPost $oDMPost): void {}) ; } - - protected static function getClass(): string - { - return ODMPost::class; - } } diff --git a/tests/Fixtures/Maker/expected/can_create_factory_with_embeddable_with_data_set_orm.php b/tests/Fixtures/Maker/expected/can_create_factory_with_embeddable_with_data_set_orm.php index 13fd09f29..69dde4dae 100644 --- a/tests/Fixtures/Maker/expected/can_create_factory_with_embeddable_with_data_set_orm.php +++ b/tests/Fixtures/Maker/expected/can_create_factory_with_embeddable_with_data_set_orm.php @@ -12,31 +12,31 @@ namespace App\Factory; use Doctrine\ORM\EntityRepository; -use Zenstruck\Foundry\ModelFactory; -use Zenstruck\Foundry\Proxy; -use Zenstruck\Foundry\RepositoryProxy; +use Zenstruck\Foundry\Persistence\PersistentProxyObjectFactory; +use Zenstruck\Foundry\Persistence\Proxy; +use Zenstruck\Foundry\Persistence\ProxyRepositoryDecorator; use Zenstruck\Foundry\Tests\Fixtures\Entity\Contact; /** - * @extends ModelFactory + * @extends PersistentProxyObjectFactory * - * @method Contact|Proxy create(array|callable $attributes = []) - * @method static Contact|Proxy createOne(array $attributes = []) - * @method static Contact|Proxy find(object|array|mixed $criteria) - * @method static Contact|Proxy findOrCreate(array $attributes) - * @method static Contact|Proxy first(string $sortedField = 'id') - * @method static Contact|Proxy last(string $sortedField = 'id') - * @method static Contact|Proxy random(array $attributes = []) - * @method static Contact|Proxy randomOrCreate(array $attributes = []) - * @method static EntityRepository|RepositoryProxy repository() - * @method static Contact[]|Proxy[] all() - * @method static Contact[]|Proxy[] createMany(int $number, array|callable $attributes = []) - * @method static Contact[]|Proxy[] createSequence(iterable|callable $sequence) - * @method static Contact[]|Proxy[] findBy(array $attributes) - * @method static Contact[]|Proxy[] randomRange(int $min, int $max, array $attributes = []) - * @method static Contact[]|Proxy[] randomSet(int $number, array $attributes = []) + * @method Contact|Proxy create(array|callable $attributes = []) + * @method static Contact|Proxy createOne(array $attributes = []) + * @method static Contact|Proxy find(object|array|mixed $criteria) + * @method static Contact|Proxy findOrCreate(array $attributes) + * @method static Contact|Proxy first(string $sortedField = 'id') + * @method static Contact|Proxy last(string $sortedField = 'id') + * @method static Contact|Proxy random(array $attributes = []) + * @method static Contact|Proxy randomOrCreate(array $attributes = []) + * @method static EntityRepository|ProxyRepositoryDecorator repository() + * @method static Contact[]|Proxy[] all() + * @method static Contact[]|Proxy[] createMany(int $number, array|callable $attributes = []) + * @method static Contact[]|Proxy[] createSequence(iterable|callable $sequence) + * @method static Contact[]|Proxy[] findBy(array $attributes) + * @method static Contact[]|Proxy[] randomRange(int $min, int $max, array $attributes = []) + * @method static Contact[]|Proxy[] randomSet(int $number, array $attributes = []) */ -final class ContactFactory extends ModelFactory +final class ContactFactory extends PersistentProxyObjectFactory { /** * @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#factories-as-services @@ -45,7 +45,11 @@ final class ContactFactory extends ModelFactory */ public function __construct() { - parent::__construct(); + } + + public static function class(): string + { + return Contact::class; } /** @@ -53,7 +57,7 @@ public function __construct() * * @todo add your default values here */ - protected function getDefaults(): array + protected function defaults(): array|callable { return [ 'address' => AddressFactory::new(), @@ -64,15 +68,10 @@ protected function getDefaults(): array /** * @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#initialization */ - protected function initialize(): self + protected function initialize(): static { return $this // ->afterInstantiate(function(Contact $contact): void {}) ; } - - protected static function getClass(): string - { - return Contact::class; - } } diff --git a/tests/Fixtures/Maker/expected/can_create_factory_with_relation_defaults.php b/tests/Fixtures/Maker/expected/can_create_factory_with_relation_defaults.php index 4422d1ca0..4fe4c45f0 100644 --- a/tests/Fixtures/Maker/expected/can_create_factory_with_relation_defaults.php +++ b/tests/Fixtures/Maker/expected/can_create_factory_with_relation_defaults.php @@ -13,32 +13,32 @@ use App\Factory\Cascade\BrandFactory; use Doctrine\ORM\EntityRepository; -use Zenstruck\Foundry\ModelFactory; -use Zenstruck\Foundry\Proxy; -use Zenstruck\Foundry\RepositoryProxy; +use Zenstruck\Foundry\Persistence\PersistentProxyObjectFactory; +use Zenstruck\Foundry\Persistence\Proxy; +use Zenstruck\Foundry\Persistence\ProxyRepositoryDecorator; use Zenstruck\Foundry\Tests\Fixtures\Entity\EntityWithRelations; use Zenstruck\Foundry\Tests\Fixtures\Factories\CategoryFactory; /** - * @extends ModelFactory + * @extends PersistentProxyObjectFactory * - * @method EntityWithRelations|Proxy create(array|callable $attributes = []) - * @method static EntityWithRelations|Proxy createOne(array $attributes = []) - * @method static EntityWithRelations|Proxy find(object|array|mixed $criteria) - * @method static EntityWithRelations|Proxy findOrCreate(array $attributes) - * @method static EntityWithRelations|Proxy first(string $sortedField = 'id') - * @method static EntityWithRelations|Proxy last(string $sortedField = 'id') - * @method static EntityWithRelations|Proxy random(array $attributes = []) - * @method static EntityWithRelations|Proxy randomOrCreate(array $attributes = []) - * @method static EntityRepository|RepositoryProxy repository() - * @method static EntityWithRelations[]|Proxy[] all() - * @method static EntityWithRelations[]|Proxy[] createMany(int $number, array|callable $attributes = []) - * @method static EntityWithRelations[]|Proxy[] createSequence(iterable|callable $sequence) - * @method static EntityWithRelations[]|Proxy[] findBy(array $attributes) - * @method static EntityWithRelations[]|Proxy[] randomRange(int $min, int $max, array $attributes = []) - * @method static EntityWithRelations[]|Proxy[] randomSet(int $number, array $attributes = []) + * @method EntityWithRelations|Proxy create(array|callable $attributes = []) + * @method static EntityWithRelations|Proxy createOne(array $attributes = []) + * @method static EntityWithRelations|Proxy find(object|array|mixed $criteria) + * @method static EntityWithRelations|Proxy findOrCreate(array $attributes) + * @method static EntityWithRelations|Proxy first(string $sortedField = 'id') + * @method static EntityWithRelations|Proxy last(string $sortedField = 'id') + * @method static EntityWithRelations|Proxy random(array $attributes = []) + * @method static EntityWithRelations|Proxy randomOrCreate(array $attributes = []) + * @method static EntityRepository|ProxyRepositoryDecorator repository() + * @method static EntityWithRelations[]|Proxy[] all() + * @method static EntityWithRelations[]|Proxy[] createMany(int $number, array|callable $attributes = []) + * @method static EntityWithRelations[]|Proxy[] createSequence(iterable|callable $sequence) + * @method static EntityWithRelations[]|Proxy[] findBy(array $attributes) + * @method static EntityWithRelations[]|Proxy[] randomRange(int $min, int $max, array $attributes = []) + * @method static EntityWithRelations[]|Proxy[] randomSet(int $number, array $attributes = []) */ -final class EntityWithRelationsFactory extends ModelFactory +final class EntityWithRelationsFactory extends PersistentProxyObjectFactory { /** * @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#factories-as-services @@ -47,7 +47,11 @@ final class EntityWithRelationsFactory extends ModelFactory */ public function __construct() { - parent::__construct(); + } + + public static function class(): string + { + return EntityWithRelations::class; } /** @@ -55,7 +59,7 @@ public function __construct() * * @todo add your default values here */ - protected function getDefaults(): array + protected function defaults(): array|callable { return [ 'manyToOne' => CategoryFactory::new(), @@ -67,15 +71,10 @@ protected function getDefaults(): array /** * @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#initialization */ - protected function initialize(): self + protected function initialize(): static { return $this // ->afterInstantiate(function(EntityWithRelations $entityWithRelations): void {}) ; } - - protected static function getClass(): string - { - return EntityWithRelations::class; - } } diff --git a/tests/Fixtures/Maker/expected/can_create_factory_with_relation_for_all_fields.php b/tests/Fixtures/Maker/expected/can_create_factory_with_relation_for_all_fields.php index 4422d1ca0..4fe4c45f0 100644 --- a/tests/Fixtures/Maker/expected/can_create_factory_with_relation_for_all_fields.php +++ b/tests/Fixtures/Maker/expected/can_create_factory_with_relation_for_all_fields.php @@ -13,32 +13,32 @@ use App\Factory\Cascade\BrandFactory; use Doctrine\ORM\EntityRepository; -use Zenstruck\Foundry\ModelFactory; -use Zenstruck\Foundry\Proxy; -use Zenstruck\Foundry\RepositoryProxy; +use Zenstruck\Foundry\Persistence\PersistentProxyObjectFactory; +use Zenstruck\Foundry\Persistence\Proxy; +use Zenstruck\Foundry\Persistence\ProxyRepositoryDecorator; use Zenstruck\Foundry\Tests\Fixtures\Entity\EntityWithRelations; use Zenstruck\Foundry\Tests\Fixtures\Factories\CategoryFactory; /** - * @extends ModelFactory + * @extends PersistentProxyObjectFactory * - * @method EntityWithRelations|Proxy create(array|callable $attributes = []) - * @method static EntityWithRelations|Proxy createOne(array $attributes = []) - * @method static EntityWithRelations|Proxy find(object|array|mixed $criteria) - * @method static EntityWithRelations|Proxy findOrCreate(array $attributes) - * @method static EntityWithRelations|Proxy first(string $sortedField = 'id') - * @method static EntityWithRelations|Proxy last(string $sortedField = 'id') - * @method static EntityWithRelations|Proxy random(array $attributes = []) - * @method static EntityWithRelations|Proxy randomOrCreate(array $attributes = []) - * @method static EntityRepository|RepositoryProxy repository() - * @method static EntityWithRelations[]|Proxy[] all() - * @method static EntityWithRelations[]|Proxy[] createMany(int $number, array|callable $attributes = []) - * @method static EntityWithRelations[]|Proxy[] createSequence(iterable|callable $sequence) - * @method static EntityWithRelations[]|Proxy[] findBy(array $attributes) - * @method static EntityWithRelations[]|Proxy[] randomRange(int $min, int $max, array $attributes = []) - * @method static EntityWithRelations[]|Proxy[] randomSet(int $number, array $attributes = []) + * @method EntityWithRelations|Proxy create(array|callable $attributes = []) + * @method static EntityWithRelations|Proxy createOne(array $attributes = []) + * @method static EntityWithRelations|Proxy find(object|array|mixed $criteria) + * @method static EntityWithRelations|Proxy findOrCreate(array $attributes) + * @method static EntityWithRelations|Proxy first(string $sortedField = 'id') + * @method static EntityWithRelations|Proxy last(string $sortedField = 'id') + * @method static EntityWithRelations|Proxy random(array $attributes = []) + * @method static EntityWithRelations|Proxy randomOrCreate(array $attributes = []) + * @method static EntityRepository|ProxyRepositoryDecorator repository() + * @method static EntityWithRelations[]|Proxy[] all() + * @method static EntityWithRelations[]|Proxy[] createMany(int $number, array|callable $attributes = []) + * @method static EntityWithRelations[]|Proxy[] createSequence(iterable|callable $sequence) + * @method static EntityWithRelations[]|Proxy[] findBy(array $attributes) + * @method static EntityWithRelations[]|Proxy[] randomRange(int $min, int $max, array $attributes = []) + * @method static EntityWithRelations[]|Proxy[] randomSet(int $number, array $attributes = []) */ -final class EntityWithRelationsFactory extends ModelFactory +final class EntityWithRelationsFactory extends PersistentProxyObjectFactory { /** * @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#factories-as-services @@ -47,7 +47,11 @@ final class EntityWithRelationsFactory extends ModelFactory */ public function __construct() { - parent::__construct(); + } + + public static function class(): string + { + return EntityWithRelations::class; } /** @@ -55,7 +59,7 @@ public function __construct() * * @todo add your default values here */ - protected function getDefaults(): array + protected function defaults(): array|callable { return [ 'manyToOne' => CategoryFactory::new(), @@ -67,15 +71,10 @@ protected function getDefaults(): array /** * @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#initialization */ - protected function initialize(): self + protected function initialize(): static { return $this // ->afterInstantiate(function(EntityWithRelations $entityWithRelations): void {}) ; } - - protected static function getClass(): string - { - return EntityWithRelations::class; - } } diff --git a/tests/Fixtures/Maker/expected/can_create_factory_with_static_analysis_annotations_with_data_set_phpstan.php b/tests/Fixtures/Maker/expected/can_create_factory_with_static_analysis_annotations_with_data_set_phpstan.php index cbf63b502..15694c910 100644 --- a/tests/Fixtures/Maker/expected/can_create_factory_with_static_analysis_annotations_with_data_set_phpstan.php +++ b/tests/Fixtures/Maker/expected/can_create_factory_with_static_analysis_annotations_with_data_set_phpstan.php @@ -12,47 +12,47 @@ namespace App\Factory; use Doctrine\ORM\EntityRepository; -use Zenstruck\Foundry\ModelFactory; -use Zenstruck\Foundry\Proxy; -use Zenstruck\Foundry\RepositoryProxy; +use Zenstruck\Foundry\Persistence\PersistentProxyObjectFactory; +use Zenstruck\Foundry\Persistence\Proxy; +use Zenstruck\Foundry\Persistence\ProxyRepositoryDecorator; use Zenstruck\Foundry\Tests\Fixtures\Entity\Category; /** - * @extends ModelFactory + * @extends PersistentProxyObjectFactory * - * @method Category|Proxy create(array|callable $attributes = []) - * @method static Category|Proxy createOne(array $attributes = []) - * @method static Category|Proxy find(object|array|mixed $criteria) - * @method static Category|Proxy findOrCreate(array $attributes) - * @method static Category|Proxy first(string $sortedField = 'id') - * @method static Category|Proxy last(string $sortedField = 'id') - * @method static Category|Proxy random(array $attributes = []) - * @method static Category|Proxy randomOrCreate(array $attributes = []) - * @method static EntityRepository|RepositoryProxy repository() - * @method static Category[]|Proxy[] all() - * @method static Category[]|Proxy[] createMany(int $number, array|callable $attributes = []) - * @method static Category[]|Proxy[] createSequence(iterable|callable $sequence) - * @method static Category[]|Proxy[] findBy(array $attributes) - * @method static Category[]|Proxy[] randomRange(int $min, int $max, array $attributes = []) - * @method static Category[]|Proxy[] randomSet(int $number, array $attributes = []) + * @method Category|Proxy create(array|callable $attributes = []) + * @method static Category|Proxy createOne(array $attributes = []) + * @method static Category|Proxy find(object|array|mixed $criteria) + * @method static Category|Proxy findOrCreate(array $attributes) + * @method static Category|Proxy first(string $sortedField = 'id') + * @method static Category|Proxy last(string $sortedField = 'id') + * @method static Category|Proxy random(array $attributes = []) + * @method static Category|Proxy randomOrCreate(array $attributes = []) + * @method static EntityRepository|ProxyRepositoryDecorator repository() + * @method static Category[]|Proxy[] all() + * @method static Category[]|Proxy[] createMany(int $number, array|callable $attributes = []) + * @method static Category[]|Proxy[] createSequence(iterable|callable $sequence) + * @method static Category[]|Proxy[] findBy(array $attributes) + * @method static Category[]|Proxy[] randomRange(int $min, int $max, array $attributes = []) + * @method static Category[]|Proxy[] randomSet(int $number, array $attributes = []) * - * @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 = []) + * @phpstan-method Category&Proxy create(array|callable $attributes = []) + * @phpstan-method static Category&Proxy createOne(array $attributes = []) + * @phpstan-method static Category&Proxy find(object|array|mixed $criteria) + * @phpstan-method static Category&Proxy findOrCreate(array $attributes) + * @phpstan-method static Category&Proxy first(string $sortedField = 'id') + * @phpstan-method static Category&Proxy last(string $sortedField = 'id') + * @phpstan-method static Category&Proxy random(array $attributes = []) + * @phpstan-method static Category&Proxy randomOrCreate(array $attributes = []) + * @phpstan-method static ProxyRepositoryDecorator 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 CategoryFactory extends ModelFactory +final class CategoryFactory extends PersistentProxyObjectFactory { /** * @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#factories-as-services @@ -61,7 +61,11 @@ final class CategoryFactory extends ModelFactory */ public function __construct() { - parent::__construct(); + } + + public static function class(): string + { + return Category::class; } /** @@ -69,7 +73,7 @@ public function __construct() * * @todo add your default values here */ - protected function getDefaults(): array + protected function defaults(): array|callable { return [ 'name' => self::faker()->text(255), @@ -79,15 +83,10 @@ protected function getDefaults(): array /** * @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#initialization */ - protected function initialize(): self + protected function initialize(): static { return $this // ->afterInstantiate(function(Category $category): void {}) ; } - - protected static function getClass(): string - { - return Category::class; - } } diff --git a/tests/Fixtures/Maker/expected/can_create_factory_with_static_analysis_annotations_with_data_set_psalm.php b/tests/Fixtures/Maker/expected/can_create_factory_with_static_analysis_annotations_with_data_set_psalm.php index ada52e12d..3da6a62a4 100644 --- a/tests/Fixtures/Maker/expected/can_create_factory_with_static_analysis_annotations_with_data_set_psalm.php +++ b/tests/Fixtures/Maker/expected/can_create_factory_with_static_analysis_annotations_with_data_set_psalm.php @@ -12,47 +12,47 @@ namespace App\Factory; use Doctrine\ORM\EntityRepository; -use Zenstruck\Foundry\ModelFactory; -use Zenstruck\Foundry\Proxy; -use Zenstruck\Foundry\RepositoryProxy; +use Zenstruck\Foundry\Persistence\PersistentProxyObjectFactory; +use Zenstruck\Foundry\Persistence\Proxy; +use Zenstruck\Foundry\Persistence\ProxyRepositoryDecorator; use Zenstruck\Foundry\Tests\Fixtures\Entity\Category; /** - * @extends ModelFactory + * @extends PersistentProxyObjectFactory * - * @method Category|Proxy create(array|callable $attributes = []) - * @method static Category|Proxy createOne(array $attributes = []) - * @method static Category|Proxy find(object|array|mixed $criteria) - * @method static Category|Proxy findOrCreate(array $attributes) - * @method static Category|Proxy first(string $sortedField = 'id') - * @method static Category|Proxy last(string $sortedField = 'id') - * @method static Category|Proxy random(array $attributes = []) - * @method static Category|Proxy randomOrCreate(array $attributes = []) - * @method static EntityRepository|RepositoryProxy repository() - * @method static Category[]|Proxy[] all() - * @method static Category[]|Proxy[] createMany(int $number, array|callable $attributes = []) - * @method static Category[]|Proxy[] createSequence(iterable|callable $sequence) - * @method static Category[]|Proxy[] findBy(array $attributes) - * @method static Category[]|Proxy[] randomRange(int $min, int $max, array $attributes = []) - * @method static Category[]|Proxy[] randomSet(int $number, array $attributes = []) + * @method Category|Proxy create(array|callable $attributes = []) + * @method static Category|Proxy createOne(array $attributes = []) + * @method static Category|Proxy find(object|array|mixed $criteria) + * @method static Category|Proxy findOrCreate(array $attributes) + * @method static Category|Proxy first(string $sortedField = 'id') + * @method static Category|Proxy last(string $sortedField = 'id') + * @method static Category|Proxy random(array $attributes = []) + * @method static Category|Proxy randomOrCreate(array $attributes = []) + * @method static EntityRepository|ProxyRepositoryDecorator repository() + * @method static Category[]|Proxy[] all() + * @method static Category[]|Proxy[] createMany(int $number, array|callable $attributes = []) + * @method static Category[]|Proxy[] createSequence(iterable|callable $sequence) + * @method static Category[]|Proxy[] findBy(array $attributes) + * @method static Category[]|Proxy[] randomRange(int $min, int $max, array $attributes = []) + * @method static Category[]|Proxy[] randomSet(int $number, array $attributes = []) * - * @psalm-method Proxy create(array|callable $attributes = []) - * @psalm-method static Proxy createOne(array $attributes = []) - * @psalm-method static Proxy find(object|array|mixed $criteria) - * @psalm-method static Proxy findOrCreate(array $attributes) - * @psalm-method static Proxy first(string $sortedField = 'id') - * @psalm-method static Proxy last(string $sortedField = 'id') - * @psalm-method static Proxy random(array $attributes = []) - * @psalm-method static Proxy randomOrCreate(array $attributes = []) - * @psalm-method static RepositoryProxy repository() - * @psalm-method static list> all() - * @psalm-method static list> createMany(int $number, array|callable $attributes = []) - * @psalm-method static list> createSequence(iterable|callable $sequence) - * @psalm-method static list> findBy(array $attributes) - * @psalm-method static list> randomRange(int $min, int $max, array $attributes = []) - * @psalm-method static list> randomSet(int $number, array $attributes = []) + * @psalm-method Category&Proxy create(array|callable $attributes = []) + * @psalm-method static Category&Proxy createOne(array $attributes = []) + * @psalm-method static Category&Proxy find(object|array|mixed $criteria) + * @psalm-method static Category&Proxy findOrCreate(array $attributes) + * @psalm-method static Category&Proxy first(string $sortedField = 'id') + * @psalm-method static Category&Proxy last(string $sortedField = 'id') + * @psalm-method static Category&Proxy random(array $attributes = []) + * @psalm-method static Category&Proxy randomOrCreate(array $attributes = []) + * @psalm-method static ProxyRepositoryDecorator repository() + * @psalm-method static list> all() + * @psalm-method static list> createMany(int $number, array|callable $attributes = []) + * @psalm-method static list> createSequence(iterable|callable $sequence) + * @psalm-method static list> findBy(array $attributes) + * @psalm-method static list> randomRange(int $min, int $max, array $attributes = []) + * @psalm-method static list> randomSet(int $number, array $attributes = []) */ -final class CategoryFactory extends ModelFactory +final class CategoryFactory extends PersistentProxyObjectFactory { /** * @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#factories-as-services @@ -61,7 +61,11 @@ final class CategoryFactory extends ModelFactory */ public function __construct() { - parent::__construct(); + } + + public static function class(): string + { + return Category::class; } /** @@ -69,7 +73,7 @@ public function __construct() * * @todo add your default values here */ - protected function getDefaults(): array + protected function defaults(): array|callable { return [ 'name' => self::faker()->text(255), @@ -79,15 +83,10 @@ protected function getDefaults(): array /** * @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#initialization */ - protected function initialize(): self + protected function initialize(): static { return $this // ->afterInstantiate(function(Category $category): void {}) ; } - - protected static function getClass(): string - { - return Category::class; - } } diff --git a/tests/Fixtures/Object/SomeObjectFactory.php b/tests/Fixtures/Object/SomeObjectFactory.php index b7e331dca..14a4a6189 100644 --- a/tests/Fixtures/Object/SomeObjectFactory.php +++ b/tests/Fixtures/Object/SomeObjectFactory.php @@ -11,20 +11,19 @@ namespace Zenstruck\Foundry\Tests\Fixtures\Object; -use Zenstruck\Foundry\ModelFactory; -use Zenstruck\Foundry\Proxy; +use Zenstruck\Foundry\ObjectFactory; /** - * @extends ModelFactory - * - * @method SomeObject|Proxy create(array|callable $attributes = []) - * @method static SomeObject|Proxy createOne(array $attributes = []) - * @method static SomeObject[]|Proxy[] createMany(int $number, array|callable $attributes = []) - * @method static SomeObject[]|Proxy[] createSequence(iterable|callable $sequence) + * @extends ObjectFactory */ -final class SomeObjectFactory extends ModelFactory +final class SomeObjectFactory extends ObjectFactory { - protected function getDefaults(): array + public static function class(): string + { + return SomeObject::class; + } + + protected function defaults(): array|callable { return [ 'arrayMandatory' => [], @@ -39,9 +38,4 @@ protected function getDefaults(): array 'stringWithDefault' => self::faker()->sentence(), ]; } - - protected static function getClass(): string - { - return SomeObject::class; - } } diff --git a/tests/Fixtures/Object/SomeOtherObjectFactory.php b/tests/Fixtures/Object/SomeOtherObjectFactory.php index 29c490e50..6b88980c2 100644 --- a/tests/Fixtures/Object/SomeOtherObjectFactory.php +++ b/tests/Fixtures/Object/SomeOtherObjectFactory.php @@ -11,27 +11,21 @@ namespace Zenstruck\Foundry\Tests\Fixtures\Object; -use Zenstruck\Foundry\ModelFactory; -use Zenstruck\Foundry\Proxy; +use Zenstruck\Foundry\ObjectFactory; /** - * @extends ModelFactory - * - * @method SomeOtherObject|Proxy create(array|callable $attributes = []) - * @method static SomeOtherObject|Proxy createOne(array $attributes = []) - * @method static SomeOtherObject[]|Proxy[] createMany(int $number, array|callable $attributes = []) - * @method static SomeOtherObject[]|Proxy[] createSequence(iterable|callable $sequence) + * @extends ObjectFactory */ -final class SomeOtherObjectFactory extends ModelFactory +final class SomeOtherObjectFactory extends ObjectFactory { - protected function getDefaults(): array + public static function class(): string { - return [ - ]; + return SomeOtherObject::class; } - protected static function getClass(): string + protected function defaults(): array|callable { - return SomeOtherObject::class; + return [ + ]; } } diff --git a/tests/Fixtures/PHP81/SomeEnum.php b/tests/Fixtures/PHP81/SomeEnum.php index 39ea0a698..104edd245 100644 --- a/tests/Fixtures/PHP81/SomeEnum.php +++ b/tests/Fixtures/PHP81/SomeEnum.php @@ -2,6 +2,15 @@ declare(strict_types=1); +/* + * This file is part of the zenstruck/foundry package. + * + * (c) Kevin Bond + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Zenstruck\Foundry\Tests\Fixtures\PHP81; enum SomeEnum: string diff --git a/tests/Fixtures/Stories/PostStory.php b/tests/Fixtures/Stories/PostStory.php index 0cafe7d1d..debdef4cb 100644 --- a/tests/Fixtures/Stories/PostStory.php +++ b/tests/Fixtures/Stories/PostStory.php @@ -29,7 +29,7 @@ public function build(): void $this->addState('postB', PostFactory::new()->create([ 'title' => 'Post B', 'category' => CategoryStory::php(), - ])->object()); + ])->_real()); $this->addState('postC', PostFactory::new([ 'title' => 'Post C', diff --git a/tests/Functional/AnonymousFactoryTest.php b/tests/Functional/AnonymousFactoryTest.php index d010d464f..614ca3adc 100644 --- a/tests/Functional/AnonymousFactoryTest.php +++ b/tests/Functional/AnonymousFactoryTest.php @@ -82,9 +82,9 @@ public function can_create_random_object_if_none_exists(): void $factory = AnonymousFactory::new($this->categoryClass(), ['name' => 'php']); $factory->assert()->count(0); - $this->assertInstanceOf($this->categoryClass(), $factory->randomOrCreate()->object()); + $this->assertInstanceOf($this->categoryClass(), $factory->randomOrCreate()->_real()); $factory->assert()->count(1); - $this->assertInstanceOf($this->categoryClass(), $factory->randomOrCreate()->object()); + $this->assertInstanceOf($this->categoryClass(), $factory->randomOrCreate()->_real()); $factory->assert()->count(1); } @@ -257,10 +257,10 @@ public function can_get_all_entities(): void $this->assertCount(4, $categories); $this->assertCount(4, \iterator_to_array($factory)); - $this->assertInstanceOf($this->categoryClass(), $categories[0]->object()); - $this->assertInstanceOf($this->categoryClass(), $categories[1]->object()); - $this->assertInstanceOf($this->categoryClass(), $categories[2]->object()); - $this->assertInstanceOf($this->categoryClass(), $categories[3]->object()); + $this->assertInstanceOf($this->categoryClass(), $categories[0]->_real()); + $this->assertInstanceOf($this->categoryClass(), $categories[1]->_real()); + $this->assertInstanceOf($this->categoryClass(), $categories[2]->_real()); + $this->assertInstanceOf($this->categoryClass(), $categories[3]->_real()); } /** @@ -284,7 +284,7 @@ public function can_find_entity(): void return; } - $this->assertSame('third', $factory->find($category->object())->getName()); + $this->assertSame('third', $factory->find($category->_real())->getName()); $this->assertSame('third', $factory->find($category)->getName()); } diff --git a/tests/Functional/FactoryDoctrineCascadeTest.php b/tests/Functional/FactoryDoctrineCascadeTest.php index 39f0d01af..712430524 100644 --- a/tests/Functional/FactoryDoctrineCascadeTest.php +++ b/tests/Functional/FactoryDoctrineCascadeTest.php @@ -12,7 +12,7 @@ namespace Zenstruck\Foundry\Tests\Functional; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; -use Zenstruck\Foundry\Instantiator; +use Zenstruck\Foundry\Object\Instantiator; use Zenstruck\Foundry\Test\Factories; use Zenstruck\Foundry\Test\ResetDatabase; use Zenstruck\Foundry\Tests\Fixtures\Entity\Cascade\Brand; @@ -23,7 +23,7 @@ use Zenstruck\Foundry\Tests\Fixtures\Entity\Cascade\Tag; use Zenstruck\Foundry\Tests\Fixtures\Entity\Cascade\Variant; -use function Zenstruck\Foundry\anonymous; +use function Zenstruck\Foundry\Persistence\persistent_factory; /** * @author Kevin Bond @@ -44,13 +44,13 @@ protected function setUp(): void */ public function many_to_one_relationship(): void { - $product = anonymous(Product::class, [ + $product = persistent_factory(Product::class, [ 'name' => 'foo', - 'brand' => anonymous(Brand::class, ['name' => 'bar']), + 'brand' => persistent_factory(Brand::class, ['name' => 'bar']), ])->instantiateWith(function(array $attributes, string $class): object { $this->assertNull($attributes['brand']->getId()); - return (new Instantiator())($attributes, $class); + return Instantiator::withConstructor()($attributes, $class); })->create(); $this->assertNotNull($product->getBrand()->getId()); @@ -62,13 +62,13 @@ public function many_to_one_relationship(): void */ public function one_to_many_relationship(): void { - $brand = anonymous(Brand::class, [ + $brand = persistent_factory(Brand::class, [ 'name' => 'brand', - 'products' => anonymous(Product::class, ['name' => 'product'])->many(2), + 'products' => persistent_factory(Product::class, ['name' => 'product'])->many(2), ])->instantiateWith(function(array $attributes, string $class): object { // $this->assertNull($attributes['products'][0]->getId()); - return (new Instantiator())($attributes, $class); + return Instantiator::withConstructor()($attributes, $class); })->create(); $this->assertCount(2, $brand->getProducts()); @@ -81,13 +81,13 @@ public function one_to_many_relationship(): void */ public function many_to_many_relationship(): void { - $product = anonymous(Product::class, [ + $product = persistent_factory(Product::class, [ 'name' => 'foo', - 'tags' => [anonymous(Tag::class, ['name' => 'bar'])], + 'tags' => [persistent_factory(Tag::class, ['name' => 'bar'])], ])->instantiateWith(function(array $attributes, string $class): object { $this->assertNull($attributes['tags'][0]->getId()); - return (new Instantiator())($attributes, $class); + return Instantiator::withConstructor()($attributes, $class); })->create(); $this->assertCount(1, $product->getTags()); @@ -100,13 +100,13 @@ public function many_to_many_relationship(): void */ public function many_to_many_reverse_relationship(): void { - $product = anonymous(Product::class, [ + $product = persistent_factory(Product::class, [ 'name' => 'foo', - 'categories' => [anonymous(ProductCategory::class, ['name' => 'bar'])], + 'categories' => [persistent_factory(ProductCategory::class, ['name' => 'bar'])], ])->instantiateWith(function(array $attributes, string $class): object { $this->assertNull($attributes['categories'][0]->getId()); - return (new Instantiator())($attributes, $class); + return Instantiator::withConstructor()($attributes, $class); })->create(); $this->assertCount(1, $product->getCategories()); @@ -119,13 +119,13 @@ public function many_to_many_reverse_relationship(): void */ public function one_to_one_relationship(): void { - $product = anonymous(Product::class, [ + $product = persistent_factory(Product::class, [ 'name' => 'foo', - 'review' => anonymous(Review::class, ['rank' => 5]), + 'review' => persistent_factory(Review::class, ['rank' => 5]), ])->instantiateWith(function(array $attributes, string $class): object { $this->assertNull($attributes['review']->getId()); - return (new Instantiator())($attributes, $class); + return Instantiator::withConstructor()($attributes, $class); })->create(); $this->assertNotNull($product->getReview()->getId()); @@ -137,13 +137,13 @@ public function one_to_one_relationship(): void */ public function one_to_one_reverse_relationship(): void { - $product = anonymous(Product::class, [ + $product = persistent_factory(Product::class, [ 'name' => 'foo', - 'review' => anonymous(Review::class, ['rank' => 4]), + 'review' => persistent_factory(Review::class, ['rank' => 4]), ])->instantiateWith(function(array $attributes, string $class): object { $this->assertNull($attributes['review']->getId()); - return (new Instantiator())($attributes, $class); + return Instantiator::withConstructor()($attributes, $class); })->create(); $this->assertNotNull($product->getReview()->getId()); @@ -155,13 +155,13 @@ public function one_to_one_reverse_relationship(): void */ public function nested_relationship_without_cascade(): void { - $product = anonymous(Product::class, [ + $product = persistent_factory(Product::class, [ 'name' => 'foo', 'variants' => [ - anonymous(Variant::class, [ + persistent_factory(Variant::class, [ 'name' => 'bar', // asserts a "sub" relationship without cascade persist is persisted - 'image' => anonymous(Image::class, ['path' => '/some/path']), + 'image' => persistent_factory(Image::class, ['path' => '/some/path']), ]), ], ])->create(); @@ -174,11 +174,11 @@ public function nested_relationship_without_cascade(): void */ public function nested_collections_with_cascade(): void { - $brand = anonymous(Brand::class, [ + $brand = persistent_factory(Brand::class, [ 'name' => 'brand', - 'products' => anonymous(Product::class, [ + 'products' => persistent_factory(Product::class, [ 'name' => 'product', - 'variants' => anonymous(Variant::class, ['name' => 'variant'])->many(3), + 'variants' => persistent_factory(Variant::class, ['name' => 'variant'])->many(3), ])->many(2), ])->create(); diff --git a/tests/Functional/FactoryTest.php b/tests/Functional/FactoryTest.php index 940168a91..f110c03f5 100644 --- a/tests/Functional/FactoryTest.php +++ b/tests/Functional/FactoryTest.php @@ -12,21 +12,22 @@ namespace Zenstruck\Foundry\Tests\Functional; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; -use Zenstruck\Foundry\Factory; -use Zenstruck\Foundry\Proxy; use Zenstruck\Foundry\Test\Factories; use Zenstruck\Foundry\Test\ResetDatabase; use Zenstruck\Foundry\Tests\Fixtures\Entity\Address; use Zenstruck\Foundry\Tests\Fixtures\Entity\Category; use Zenstruck\Foundry\Tests\Fixtures\Entity\Post; use Zenstruck\Foundry\Tests\Fixtures\Entity\Tag; +use Zenstruck\Foundry\Tests\Fixtures\Factories\PostFactory; use Zenstruck\Foundry\Tests\Fixtures\Object\SomeObject; use Zenstruck\Foundry\Tests\Fixtures\Object\SomeObjectFactory; use Zenstruck\Foundry\Tests\Fixtures\Object\SomeOtherObject; -use function Zenstruck\Foundry\anonymous; -use function Zenstruck\Foundry\create; -use function Zenstruck\Foundry\repository; +use function Zenstruck\Foundry\object; +use function Zenstruck\Foundry\Persistence\flush_after; +use function Zenstruck\Foundry\Persistence\persist; +use function Zenstruck\Foundry\Persistence\persistent_factory; +use function Zenstruck\Foundry\Persistence\repository; /** * @author Kevin Bond @@ -47,10 +48,10 @@ protected function setUp(): void */ public function many_to_one_relationship(): void { - $categoryFactory = anonymous(Category::class, ['name' => 'foo']); - $category = create(Category::class, ['name' => 'bar']); - $postA = create(Post::class, ['title' => 'title', 'body' => 'body', 'category' => $categoryFactory]); - $postB = create(Post::class, ['title' => 'title', 'body' => 'body', 'category' => $category]); + $categoryFactory = persistent_factory(Category::class, ['name' => 'foo']); + $category = persist(Category::class, ['name' => 'bar']); + $postA = persist(Post::class, ['title' => 'title', 'body' => 'body', 'category' => $categoryFactory]); + $postB = persist(Post::class, ['title' => 'title', 'body' => 'body', 'category' => $category]); $this->assertSame('foo', $postA->getCategory()->getName()); $this->assertSame('bar', $postB->getCategory()->getName()); @@ -61,11 +62,11 @@ public function many_to_one_relationship(): void */ public function one_to_many_relationship(): void { - $category = create(Category::class, [ + $category = persist(Category::class, [ 'name' => 'bar', 'posts' => [ - anonymous(Post::class, ['title' => 'Post A', 'body' => 'body']), - create(Post::class, ['title' => 'Post B', 'body' => 'body']), + persistent_factory(Post::class, ['title' => 'Post A', 'body' => 'body']), + persist(Post::class, ['title' => 'Post B', 'body' => 'body']), ], ]); @@ -84,12 +85,12 @@ public function one_to_many_relationship(): void */ public function many_to_many_relationship(): void { - $post = create(Post::class, [ + $post = persist(Post::class, [ 'title' => 'title', 'body' => 'body', 'tags' => [ - anonymous(Tag::class, ['name' => 'Tag A']), - create(Tag::class, ['name' => 'Tag B']), + persistent_factory(Tag::class, ['name' => 'Tag A']), + persist(Tag::class, ['name' => 'Tag B']), ], ]); @@ -108,11 +109,11 @@ public function many_to_many_relationship(): void */ public function many_to_many_reverse_relationship(): void { - $tag = create(Tag::class, [ + $tag = persist(Tag::class, [ 'name' => 'bar', 'posts' => [ - anonymous(Post::class, ['title' => 'Post A', 'body' => 'body']), - create(Post::class, ['title' => 'Post B', 'body' => 'body']), + persistent_factory(Post::class, ['title' => 'Post A', 'body' => 'body']), + persist(Post::class, ['title' => 'Post B', 'body' => 'body']), ], ]); @@ -131,10 +132,10 @@ public function many_to_many_reverse_relationship(): void */ public function creating_with_factory_attribute_persists_the_factory(): void { - $object = anonymous(Post::class)->create([ + $object = persistent_factory(Post::class)->create([ 'title' => 'title', 'body' => 'body', - 'category' => anonymous(Category::class, ['name' => 'name']), + 'category' => persistent_factory(Category::class, ['name' => 'name']), ]); $this->assertNotNull($object->getCategory()->getId()); @@ -145,7 +146,7 @@ public function creating_with_factory_attribute_persists_the_factory(): void */ public function can_create_embeddable(): void { - $object = anonymous(Address::class)->create(['value' => 'an address']); + $object = persistent_factory(Address::class)->create(['value' => 'an address']); $this->assertSame('an address', $object->getValue()); } @@ -155,21 +156,16 @@ public function can_delay_flush(): void repository(Post::class)->assert()->empty(); repository(Category::class)->assert()->empty(); - $post = null; - $return = Factory::delayFlush(static function() use (&$post): Proxy { - $post = anonymous(Post::class)->create([ + flush_after(static function(): void { + persistent_factory(Post::class)->create([ 'title' => 'title', 'body' => 'body', - 'category' => anonymous(Category::class, ['name' => 'name']), + 'category' => persistent_factory(Category::class, ['name' => 'name']), ]); repository(Post::class)->assert()->empty(); repository(Category::class)->assert()->empty(); - - return $post; }); - $this->assertSame($post, $return); - repository(Post::class)->assert()->count(1); repository(Category::class)->assert()->count(1); } @@ -182,23 +178,18 @@ public function auto_refresh_is_disabled_during_delay_flush(): void repository(Post::class)->assert()->empty(); repository(Category::class)->assert()->empty(); - $post = null; - $return = Factory::delayFlush(static function() use (&$post): Proxy { - $post = anonymous(Post::class)->create([ + flush_after(static function(): void { + $post = persistent_factory(Post::class)->create([ 'title' => 'title', 'body' => 'body', - 'category' => anonymous(Category::class, ['name' => 'name']), + 'category' => persistent_factory(Category::class, ['name' => 'name']), ]); $post->setTitle('new title'); $post->setBody('new body'); repository(Post::class)->assert()->empty(); repository(Category::class)->assert()->empty(); - - return $post; }); - $this->assertSame($post, $return); - repository(Post::class)->assert()->count(1); repository(Category::class)->assert()->count(1); } @@ -208,8 +199,20 @@ public function auto_refresh_is_disabled_during_delay_flush(): void */ public function can_create_an_object_not_persisted_with_nested_factory(): void { - $notPersistedObject = SomeObjectFactory::new()->create()->object(); + $notPersistedObject = SomeObjectFactory::new()->create(); self::assertInstanceOf(SomeObject::class, $notPersistedObject); self::assertInstanceOf(SomeOtherObject::class, $notPersistedObject->someOtherObjectMandatory); } + + /** + * @test + */ + public function instantiate(): void + { + $object = object(Post::class, ['title' => 'title', 'body' => 'body']); + + $this->assertInstanceOf(Post::class, $object); + PostFactory::assert()->count(0); + $this->assertSame('title', $object->getTitle()); + } } diff --git a/tests/Functional/FakerTest.php b/tests/Functional/FakerTest.php index 515a01eaf..f1687cd3a 100644 --- a/tests/Functional/FakerTest.php +++ b/tests/Functional/FakerTest.php @@ -12,9 +12,10 @@ namespace Zenstruck\Foundry\Tests\Functional; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; -use Zenstruck\Foundry\Factory; use Zenstruck\Foundry\Test\Factories; +use function Zenstruck\Foundry\faker; + /** * @author Kevin Bond */ @@ -34,6 +35,6 @@ protected function setUp(): void */ public function can_use_custom_provider(): void { - $this->assertSame('custom-value', Factory::faker()->customValue()); + $this->assertSame('custom-value', faker()->customValue()); } } diff --git a/tests/Functional/ModelFactoryTest.php b/tests/Functional/ModelFactoryTest.php index 59b3a7af2..25ba6b759 100644 --- a/tests/Functional/ModelFactoryTest.php +++ b/tests/Functional/ModelFactoryTest.php @@ -14,11 +14,14 @@ use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; use Zenstruck\Foundry\Factory; use Zenstruck\Foundry\FactoryCollection; +use Zenstruck\Foundry\RepositoryAssertions; use Zenstruck\Foundry\Test\Factories; use Zenstruck\Foundry\Test\ResetDatabase; -use function Zenstruck\Foundry\anonymous; -use function Zenstruck\Foundry\create; +use function Zenstruck\Foundry\Persistence\disable_persisting; +use function Zenstruck\Foundry\Persistence\enable_persisting; +use function Zenstruck\Foundry\Persistence\persist; +use function Zenstruck\Foundry\Persistence\persistent_factory; /** * @author Kevin Bond @@ -67,9 +70,9 @@ public function can_create_random_object_if_none_exists(): void $categoryFactoryClass = $this->categoryFactoryClass(); $categoryFactoryClass::assert()->count(0); - $this->assertInstanceOf($this->categoryClass(), $categoryFactoryClass::randomOrCreate()->object()); + $this->assertInstanceOf($this->categoryClass(), $categoryFactoryClass::randomOrCreate()->_real()); $categoryFactoryClass::assert()->count(1); - $this->assertInstanceOf($this->categoryClass(), $categoryFactoryClass::randomOrCreate()->object()); + $this->assertInstanceOf($this->categoryClass(), $categoryFactoryClass::randomOrCreate()->_real()); $categoryFactoryClass::assert()->count(1); } @@ -234,10 +237,10 @@ public function can_get_all_entities(): void $categories = $categoryFactoryClass::all(); $this->assertCount(4, $categories); - $this->assertInstanceOf($this->categoryClass(), $categories[0]->object()); - $this->assertInstanceOf($this->categoryClass(), $categories[1]->object()); - $this->assertInstanceOf($this->categoryClass(), $categories[2]->object()); - $this->assertInstanceOf($this->categoryClass(), $categories[3]->object()); + $this->assertInstanceOf($this->categoryClass(), $categories[0]->_real()); + $this->assertInstanceOf($this->categoryClass(), $categories[1]->_real()); + $this->assertInstanceOf($this->categoryClass(), $categories[2]->_real()); + $this->assertInstanceOf($this->categoryClass(), $categories[3]->_real()); } /** @@ -257,7 +260,7 @@ public function can_find_entity(): void if ($this instanceof ORMModelFactoryTest) { $this->assertSame('third', $categoryFactoryClass::find($category)->getName()); - $this->assertSame('third', $categoryFactoryClass::find($category->object())->getName()); + $this->assertSame('third', $categoryFactoryClass::find($category->_real())->getName()); } } @@ -311,9 +314,20 @@ public function resave_after_persist_events(): void /** * @test + */ + public function can_create_sequence(): void + { + $categoryFactoryClass = $this->categoryFactoryClass(); + $categoryFactoryClass::createSequence([['name' => 'foo'], ['name' => 'bar']]); + + $categoryFactoryClass::assert()->exists(['name' => 'foo']); + $categoryFactoryClass::assert()->exists(['name' => 'bar']); + } + + /** * @dataProvider sequenceProvider */ - public function can_create_sequence(\Closure|string|array $sequence): void + public function can_create_sequence_with_callable(callable $sequence): void { $categoryFactoryClass = $this->categoryFactoryClass(); $categoryFactoryClass::createSequence($sequence); @@ -324,10 +338,6 @@ public function can_create_sequence(\Closure|string|array $sequence): void public function sequenceProvider(): iterable { - yield 'with array of attributes' => [ - [['name' => 'foo'], ['name' => 'bar']], - ]; - yield 'with a callable which returns an array of attributes' => [ static fn(): array => [['name' => 'foo'], ['name' => 'bar']], ]; @@ -361,7 +371,7 @@ public function can_create_many_objects_with_index(): void public function can_use_factory_collection_as_data_provider(FactoryCollection $factoryCollection): void { $factoryCollection->create(); - $factoryCollection->factory()::assert()->exists(['name' => 'foo']); + $factoryCollection->factory::assert()->exists(['name' => 'foo']); } public function factoryCollectionAsDataProvider(): iterable @@ -433,16 +443,16 @@ public function can_pass_method_as_attributes(): void */ public function can_disable_persist_globally(): void { - $this->disablePersist(); + disable_persisting(); $categoryFactoryClass = $this->categoryFactoryClass(); $categoryFactoryClass::createOne(['name' => 'foo']); $categoryFactoryClass::new()->create(['name' => 'foo']); - anonymous($this->categoryClass())->create(['name' => 'foo']); - create($this->categoryClass(), ['name' => 'foo']); + persistent_factory($this->categoryClass())->create(['name' => 'foo']); + persist($this->categoryClass(), ['name' => 'foo']); - $this->enablePersist(); // need to reactivate persist to access to RepositoryAssertions + enable_persisting(); // need to reactivate persist to access to RepositoryAssertions $categoryFactoryClass::assert()->count(0); } @@ -451,7 +461,7 @@ public function can_disable_persist_globally(): void */ public function cannot_access_repository_method_when_persist_disabled(): void { - $this->disablePersist(); + disable_persisting(); $countErrors = 0; try { @@ -483,7 +493,7 @@ public function assert_persist_is_re_enabled_automatically(): void { self::assertTrue(Factory::configuration()->isPersistEnabled()); - create($this->categoryClass(), ['name' => 'foo']); + persist($this->categoryClass(), ['name' => 'foo']); $this->categoryFactoryClass()::assert()->count(1); } diff --git a/tests/Functional/ODMModelFactoryTest.php b/tests/Functional/ODMModelFactoryTest.php index 0cd877732..bd81415ee 100644 --- a/tests/Functional/ODMModelFactoryTest.php +++ b/tests/Functional/ODMModelFactoryTest.php @@ -11,7 +11,8 @@ namespace Zenstruck\Foundry\Tests\Functional; -use Zenstruck\Foundry\Proxy; +use Zenstruck\Foundry\Factory; +use Zenstruck\Foundry\Persistence\Proxy; use Zenstruck\Foundry\Tests\Fixtures\Document\ODMCategory; use Zenstruck\Foundry\Tests\Fixtures\Document\ODMComment; use Zenstruck\Foundry\Tests\Fixtures\Document\ODMPost; @@ -40,9 +41,8 @@ public function can_use_factory_for_embedded_object(): void { $proxyObject = CommentFactory::createOne(['user' => new ODMUser('some user'), 'body' => 'some body']); self::assertInstanceOf(Proxy::class, $proxyObject); - self::assertFalse($proxyObject->isPersisted()); - $comment = $proxyObject->object(); + $comment = $proxyObject->_real(); self::assertInstanceOf(ODMComment::class, $comment); self::assertEquals(new ODMUser('some user'), $comment->getUser()); self::assertSame('some body', $comment->getBody()); @@ -61,7 +61,7 @@ public function can_hydrate_embed_many_fields(): void $posts = PostFactory::findBy(['title' => 'foo']); self::assertCount(1, $posts); - $post = $posts[0]->object(); + $post = $posts[0]->_real(); self::assertInstanceOf(ODMPost::class, $post); self::assertCount(4, $post->getComments()); self::assertContainsOnlyInstancesOf(ODMComment::class, $post->getComments()); @@ -90,41 +90,49 @@ public function can_find_or_create_from_embedded_object(): void $post2 = PostFactory::findOrCreate(['title' => 'foo', 'user' => new ODMUser('some user')]); PostFactory::assert()->count(1); - self::assertSame($post->object(), $post2->object()); + self::assertSame($post->_real(), $post2->_real()); } /** * @test + * @group legacy */ public function can_find_or_create_from_object(): void { + // must disable auto refresh proxy: otherwise doctrine would update post from db and recreate the user object. + Factory::configuration()->disableDefaultProxyAutoRefresh(); + $user = UserFactory::createOne(['name' => 'some user']); - $post = PostFactory::findOrCreate($attributes = ['user' => $user->object()]); + $post = PostFactory::findOrCreate($attributes = ['user' => $user->_real()]); - self::assertSame($user->object(), $post->getUser()); + self::assertSame($user->_real(), $post->getUser()); PostFactory::assert()->count(1); $post2 = PostFactory::findOrCreate($attributes); PostFactory::assert()->count(1); - self::assertSame($post->object(), $post2->object()); + self::assertSame($post->_real(), $post2->_real()); } /** * @test + * @group legacy */ public function can_find_or_create_from_proxy_of_object(): void { + // must disable auto refresh proxy: otherwise doctrine would update post from db and recreate the user object. + Factory::configuration()->disableDefaultProxyAutoRefresh(); + $user = UserFactory::createOne(['name' => 'some user']); $post = PostFactory::findOrCreate($attributes = ['user' => $user]); - self::assertSame($user->object(), $post->getUser()); + self::assertSame($user->_real(), $post->getUser()); PostFactory::assert()->count(1); $post2 = PostFactory::findOrCreate($attributes); PostFactory::assert()->count(1); - self::assertSame($post->object(), $post2->object()); + self::assertSame($post->_real(), $post2->_real()); } /** diff --git a/tests/Functional/ODMRepositoryProxyTest.php b/tests/Functional/ODMRepositoryDecoratorTest.php similarity index 92% rename from tests/Functional/ODMRepositoryProxyTest.php rename to tests/Functional/ODMRepositoryDecoratorTest.php index e84ac223a..d375f7187 100644 --- a/tests/Functional/ODMRepositoryProxyTest.php +++ b/tests/Functional/ODMRepositoryDecoratorTest.php @@ -17,7 +17,7 @@ /** * @author Kevin Bond */ -final class ODMRepositoryProxyTest extends RepositoryProxyTest +final class ODMRepositoryDecoratorTest extends RepositoryDecoratorTest { protected function setUp(): void { diff --git a/tests/Functional/ORMModelFactoryTest.php b/tests/Functional/ORMModelFactoryTest.php index 62fa652f4..82f8141f6 100644 --- a/tests/Functional/ORMModelFactoryTest.php +++ b/tests/Functional/ORMModelFactoryTest.php @@ -53,6 +53,7 @@ public function can_override_initialize(): void /** * @test + * @group legacy */ public function initialize_must_return_an_instance_of_the_current_factory(): void { @@ -64,6 +65,7 @@ public function initialize_must_return_an_instance_of_the_current_factory(): voi /** * @test + * @group legacy */ public function initialize_must_return_a_value(): void { @@ -481,7 +483,6 @@ public function can_use_model_factories_in_a_data_provider(PostFactory $factory, { $post = $factory->create(); - $post->assertPersisted(); $this->assertSame($published, $post->isPublished()); } @@ -512,7 +513,7 @@ public function many_to_one_unmanaged_entity(): void */ public function many_to_one_unmanaged_raw_entity(): void { - $category = CategoryFactory::createOne(['name' => 'My Category'])->object(); + $category = CategoryFactory::createOne(['name' => 'My Category'])->_real(); self::getContainer()->get(EntityManagerInterface::class)->clear(); @@ -552,7 +553,7 @@ public function embeddables_are_never_persisted(): void */ public function can_count_based_on_a_relationship(): void { - $category = CategoryFactory::createOne()->object(); + $category = CategoryFactory::createOne()->_real(); PostFactory::createMany(2, ['category' => $category]); PostFactory::createMany(2); @@ -573,7 +574,7 @@ public function can_find_or_create_from_embedded_object(): void $contact2 = ContactFactory::findOrCreate($attributes); ContactFactory::assert()->count(1); - self::assertSame($contact->object(), $contact2->object()); + self::assertSame($contact->_real(), $contact2->_real()); } /** @@ -588,7 +589,7 @@ public function can_find_or_create_from_factory_of_embedded_object(): void $contact2 = ContactFactory::findOrCreate($attributes); ContactFactory::assert()->count(1); - self::assertSame($contact->object(), $contact2->object()); + self::assertSame($contact->_real(), $contact2->_real()); } /** @@ -597,15 +598,15 @@ public function can_find_or_create_from_factory_of_embedded_object(): void public function can_find_or_create_from_object(): void { $user = UserFactory::createOne(); - $comment = CommentFactory::findOrCreate($attributes = ['user' => $user->object(), 'createdAt' => new \DateTime('2023-01-01')]); + $comment = CommentFactory::findOrCreate($attributes = ['user' => $user->_real(), 'createdAt' => new \DateTime('2023-01-01')]); - self::assertSame($user->object(), $comment->getUser()); + self::assertSame($user->_real(), $comment->getUser()); CommentFactory::assert()->count(1); $comment2 = CommentFactory::findOrCreate($attributes); CommentFactory::assert()->count(1); - self::assertSame($comment->object(), $comment2->object()); + self::assertSame($comment->_real(), $comment2->_real()); } /** @@ -616,13 +617,13 @@ public function can_find_or_create_from_proxy_of_object(): void $user = UserFactory::createOne(); $comment = CommentFactory::findOrCreate($attributes = ['user' => $user]); - self::assertSame($user->object(), $comment->getUser()); + self::assertSame($user->_real(), $comment->getUser()); CommentFactory::assert()->count(1); $comment2 = CommentFactory::findOrCreate($attributes); CommentFactory::assert()->count(1); - self::assertSame($comment->object(), $comment2->object()); + self::assertSame($comment->_real(), $comment2->_real()); } /** @@ -637,7 +638,7 @@ public function can_find_or_create_entity_with_enum(): void $entityWithEnum2 = EntityWithEnumFactory::findOrCreate($attributes); EntityWithEnumFactory::assert()->count(1); - self::assertSame($entityWithEnum->object(), $entityWithEnum2->object()); + self::assertSame($entityWithEnum->_real(), $entityWithEnum2->_real()); } /** @@ -680,6 +681,7 @@ public static function addOneToManyWithExtraAttributes(): iterable /** * @test + * @group legacy */ public function it_can_create_entity_with_property_name_different_from_constructor_name(): void { diff --git a/tests/Functional/ORMProxyTest.php b/tests/Functional/ORMProxyTest.php index 1436ec990..c915eebe6 100644 --- a/tests/Functional/ORMProxyTest.php +++ b/tests/Functional/ORMProxyTest.php @@ -17,7 +17,7 @@ use Zenstruck\Foundry\Tests\Fixtures\Factories\CategoryFactory; use Zenstruck\Foundry\Tests\Fixtures\Factories\PostFactory; -use function Zenstruck\Foundry\anonymous; +use function Zenstruck\Foundry\Persistence\proxy_factory; /** * @author Kevin Bond @@ -47,8 +47,8 @@ public function cannot_convert_to_string_if_underlying_object_cant(): void */ public function can_autorefresh_entity_with_embedded_object(): void { - $contact = anonymous(Contact::class)->create(['name' => 'john']) - ->enableAutoRefresh() + $contact = proxy_factory(Contact::class)->create(['name' => 'john']) + ->_enableAutoRefresh() ; $this->assertSame('john', $contact->getName()); @@ -60,7 +60,7 @@ public function can_autorefresh_entity_with_embedded_object(): void $this->assertSame('some address', $contact->getAddress()->getValue()); $contact->getAddress()->setValue('address'); - $contact->save(); + $contact->_save(); $this->assertSame('address', $contact->getAddress()->getValue()); diff --git a/tests/Functional/ORMRepositoryProxyTest.php b/tests/Functional/ORMRepositoryDecoratorTest.php similarity index 93% rename from tests/Functional/ORMRepositoryProxyTest.php rename to tests/Functional/ORMRepositoryDecoratorTest.php index 0bbd446f1..ff6e5ffba 100644 --- a/tests/Functional/ORMRepositoryProxyTest.php +++ b/tests/Functional/ORMRepositoryDecoratorTest.php @@ -18,12 +18,12 @@ use Zenstruck\Foundry\Tests\Fixtures\Factories\CategoryFactory; use Zenstruck\Foundry\Tests\Fixtures\Factories\PostFactory; -use function Zenstruck\Foundry\repository; +use function Zenstruck\Foundry\Persistence\repository; /** * @author Kevin Bond */ -final class ORMRepositoryProxyTest extends RepositoryProxyTest +final class ORMRepositoryDecoratorTest extends RepositoryDecoratorTest { protected function setUp(): void { @@ -56,7 +56,7 @@ public function doctrine_proxies_are_converted_to_foundry_proxies(): void PostFactory::random(); // load a random Category which should be a "doctrine proxy" - $category = CategoryFactory::random()->object(); + $category = CategoryFactory::random()->_real(); // ensure the category is a "doctrine proxy" and a Category if (\interface_exists(DoctrineProxy::class)) { @@ -70,6 +70,7 @@ public function doctrine_proxies_are_converted_to_foundry_proxies(): void /** * @test + * @group legacy */ public function proxy_wrapping_orm_entity_manager_can_order_by_in_find_one_by(): void { diff --git a/tests/Functional/PersistentObjectFactoryTest.php b/tests/Functional/PersistentObjectFactoryTest.php new file mode 100644 index 000000000..883a1786b --- /dev/null +++ b/tests/Functional/PersistentObjectFactoryTest.php @@ -0,0 +1,219 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Functional; + +use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; +use Zenstruck\Foundry\Test\Factories; +use Zenstruck\Foundry\Test\ResetDatabase; +use Zenstruck\Foundry\Tests\Fixtures\Entity\Post; +use Zenstruck\Foundry\Tests\Fixtures\Factories\PostFactoryNoProxy; + +final class PersistentObjectFactoryTest extends KernelTestCase +{ + use Factories, ResetDatabase; + + protected function setUp(): void + { + if (!\getenv('DATABASE_URL')) { + self::markTestSkipped('doctrine/orm not enabled.'); + } + } + + /** + * @test + */ + public function can_create_one(): void + { + $post = PostFactoryNoProxy::createOne(); + + self::assertInstanceOf(Post::class, $post); + + PostFactoryNoProxy::assert()->count(1); + } + + /** + * @test + */ + public function can_create_one_from_instance_method(): void + { + $post = PostFactoryNoProxy::new()->create(['title' => 'foo']); + + self::assertInstanceOf(Post::class, $post); + + PostFactoryNoProxy::assert()->count(1); + } + + /** + * @test + */ + public function can_create_many(): void + { + $posts = PostFactoryNoProxy::createMany(2); + + self::assertCount(2, $posts); + self::assertContainsOnlyInstancesOf(Post::class, $posts); + + PostFactoryNoProxy::assert()->count(2); + } + + /** + * @test + */ + public function can_create_many_from_instance_method(): void + { + $posts = PostFactoryNoProxy::new()->many(2)->create(['title' => 'foo']); + + self::assertCount(2, $posts); + self::assertContainsOnlyInstancesOf(Post::class, $posts); + + PostFactoryNoProxy::assert()->count(2); + } + + /** + * @test + */ + public function can_create_sequence(): void + { + $posts = PostFactoryNoProxy::createSequence([[], []]); + + self::assertCount(2, $posts); + self::assertContainsOnlyInstancesOf(Post::class, $posts); + + PostFactoryNoProxy::assert()->count(2); + } + + /** + * @test + */ + public function can_find_or_create(): void + { + $postCreated = PostFactoryNoProxy::findOrCreate([]); + $postFetchedFromDb = PostFactoryNoProxy::findOrCreate([]); + + self::assertInstanceOf(Post::class, $postCreated); + self::assertSame($postCreated, $postFetchedFromDb); + + PostFactoryNoProxy::assert()->count(1); + } + + /** + * @test + */ + public function can_get_first_item(): void + { + PostFactoryNoProxy::createSequence([['title' => 'foo'], ['title' => 'bar']]); + $post = PostFactoryNoProxy::first(); + + self::assertInstanceOf(Post::class, $post); + self::assertSame('foo', $post->getTitle()); + } + + /** + * @test + */ + public function can_get_last_item(): void + { + PostFactoryNoProxy::createSequence([['title' => 'foo'], ['title' => 'bar']]); + $post = PostFactoryNoProxy::last(); + + self::assertInstanceOf(Post::class, $post); + self::assertSame('bar', $post->getTitle()); + } + + /** + * @test + */ + public function can_get_random_item(): void + { + PostFactoryNoProxy::createSequence([['title' => 'foo'], ['title' => 'bar']]); + $post = PostFactoryNoProxy::random(); + + self::assertInstanceOf(Post::class, $post); + } + + /** + * @test + */ + public function can_get_or_create_random_item(): void + { + $postCreated = PostFactoryNoProxy::randomOrCreate([]); + $postFetchedFromDb = PostFactoryNoProxy::randomOrCreate([]); + + self::assertInstanceOf(Post::class, $postCreated); + self::assertSame($postCreated, $postFetchedFromDb); + + PostFactoryNoProxy::assert()->count(1); + } + + /** + * @test + */ + public function can_get_random_set(): void + { + PostFactoryNoProxy::createSequence([['title' => 'foo'], ['title' => 'bar'], ['title' => 'baz']]); + $posts = PostFactoryNoProxy::randomSet(2); + + self::assertCount(2, $posts); + self::assertContainsOnlyInstancesOf(Post::class, $posts); + } + + /** + * @test + */ + public function can_get_random_range(): void + { + PostFactoryNoProxy::createSequence([['title' => 'foo'], ['title' => 'bar'], ['title' => 'baz']]); + $posts = PostFactoryNoProxy::randomRange(1, 2); + + self::assertGreaterThanOrEqual(1, \count($posts)); + self::assertLessThanOrEqual(2, \count($posts)); + self::assertContainsOnlyInstancesOf(Post::class, $posts); + } + + /** + * @test + */ + public function can_get_all(): void + { + PostFactoryNoProxy::createSequence([['title' => 'foo'], ['title' => 'bar']]); + $posts = PostFactoryNoProxy::all(); + + self::assertCount(2, $posts); + self::assertContainsOnlyInstancesOf(Post::class, $posts); + } + + /** + * @test + */ + public function can_find_item(): void + { + PostFactoryNoProxy::createOne(['title' => 'foo']); + $post = PostFactoryNoProxy::find(['title' => 'foo']); + + self::assertInstanceOf(Post::class, $post); + self::assertSame('foo', $post->getTitle()); + } + + /** + * @test + */ + public function can_find_by_item(): void + { + PostFactoryNoProxy::createMany(2, ['title' => 'foo']); + $posts = PostFactoryNoProxy::findBy(['title' => 'foo']); + + self::assertCount(2, $posts); + self::assertContainsOnlyInstancesOf(Post::class, $posts); + } +} diff --git a/tests/Functional/ProxyTest.php b/tests/Functional/ProxyTest.php index 95e5240bf..acad02344 100644 --- a/tests/Functional/ProxyTest.php +++ b/tests/Functional/ProxyTest.php @@ -14,9 +14,11 @@ use PHPUnit\Framework\AssertionFailedError; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; use Zenstruck\Assert; -use Zenstruck\Foundry\Proxy; +use Zenstruck\Foundry\Persistence\PersistentProxyObjectFactory; +use Zenstruck\Foundry\Persistence\Proxy; use Zenstruck\Foundry\Test\Factories; use Zenstruck\Foundry\Test\ResetDatabase; +use Zenstruck\Foundry\Tests\Fixtures\Entity\Post; /** * @author Kevin Bond @@ -27,6 +29,7 @@ abstract class ProxyTest extends KernelTestCase /** * @test + * @group legacy */ public function can_assert_persisted(): void { @@ -39,6 +42,7 @@ public function can_assert_persisted(): void /** * @test + * @group legacy */ public function can_assert_not_persisted(): void { @@ -51,12 +55,13 @@ public function can_assert_not_persisted(): void /** * @test + * @group legacy */ public function can_remove_and_assert_not_persisted(): void { $post = $this->postFactoryClass()::createOne(); - $post->remove(); + $post->_delete(); $post->assertNotPersisted(); } @@ -73,6 +78,7 @@ public function functions_are_passed_to_wrapped_object(): void /** * @test + * @group legacy */ public function can_convert_to_string_if_wrapped_object_can(): void { @@ -90,7 +96,7 @@ public function can_refetch_object_if_object_manager_has_been_cleared(): void self::getContainer()->get($this->registryServiceId())->getManager()->clear(); - $this->assertSame('my title', $post->refresh()->getTitle()); + $this->assertSame('my title', $post->_refresh()->getTitle()); } /** @@ -109,7 +115,7 @@ public function exception_thrown_if_trying_to_refresh_deleted_object(): void $this->expectException(\RuntimeException::class); $this->expectExceptionMessage('The object no longer exists.'); - $post->refresh(); + $post->_refresh(); } /** @@ -119,11 +125,11 @@ public function can_force_set_and_save(): void { $post = $this->postFactoryClass()::createOne(['title' => 'old title']); - $post->repository()->assert()->notExists(['title' => 'new title']); + $post->_repository()->assert()->notExists(['title' => 'new title']); - $post->forceSet('title', 'new title')->save(); + $post->_set('title', 'new title')->_save(); - $post->repository()->assert()->exists(['title' => 'new title']); + $post->_repository()->assert()->exists(['title' => 'new title']); } /** @@ -131,51 +137,30 @@ public function can_force_set_and_save(): void */ public function can_force_set_multiple_fields(): void { + /** @var Proxy $post */ $post = $this->postFactoryClass()::createOne(['title' => 'old title', 'body' => 'old body']); + $post->_disableAutoRefresh(); $this->assertSame('old title', $post->getTitle()); $this->assertSame('old body', $post->getBody()); $post - ->forceSet('title', 'new title') - ->forceSet('body', 'new body') - ->save() + ->_set('title', 'new title') + ->_set('body', 'new body') + ->_save() ; $this->assertSame('new title', $post->getTitle()); $this->assertSame('new body', $post->getBody()); } - /** - * @test - */ - public function exception_thrown_if_trying_to_autorefresh_object_with_unsaved_changes(): void - { - $post = $this->postFactoryClass()::createOne(['title' => 'old title', 'body' => 'old body']) - ->enableAutoRefresh() - ; - - $this->assertSame('old title', $post->getTitle()); - $this->assertSame('old body', $post->getBody()); - - $post - ->enableAutoRefresh() - ->forceSet('title', 'new title') - ; - - $this->expectException(\RuntimeException::class); - - // exception thrown because of "unsaved changes" to $post from above - $post->forceSet('body', 'new body'); - } - /** * @test */ public function can_autorefresh_between_kernel_boots(): void { $post = $this->postFactoryClass()::createOne(['title' => 'old title', 'body' => 'old body']) - ->enableAutoRefresh() + ->_enableAutoRefresh() ; $this->assertSame('old title', $post->getTitle()); @@ -191,6 +176,7 @@ public function can_autorefresh_between_kernel_boots(): void /** * @test + * @group legacy */ public function force_set_all_solves_the_auto_refresh_problem(): void { @@ -200,12 +186,12 @@ public function force_set_all_solves_the_auto_refresh_problem(): void $this->assertSame('old body', $post->getBody()); $post - ->enableAutoRefresh() + ->_enableAutoRefresh() ->forceSetAll([ 'title' => 'new title', 'body' => 'new body', ]) - ->save() + ->_save() ; $this->assertSame('new title', $post->getTitle()); @@ -223,14 +209,14 @@ public function without_auto_refresh_solves_the_auto_refresh_problem(): void $this->assertSame('old body', $post->getBody()); $post - ->enableAutoRefresh() - ->withoutAutoRefresh(static function(Proxy $proxy): void { + ->_enableAutoRefresh() + ->_withoutAutoRefresh(static function(Proxy $proxy): void { $proxy - ->forceSet('title', 'new title') - ->forceSet('body', 'new body') + ->_set('title', 'new title') + ->_set('body', 'new body') ; }) - ->save() + ->_save() ; $this->assertSame('new title', $post->getTitle()); @@ -243,20 +229,21 @@ public function without_auto_refresh_solves_the_auto_refresh_problem(): void public function without_auto_refresh_does_not_enable_auto_refresh_if_it_was_disabled_originally(): void { $post = $this->postFactoryClass()::createOne(['title' => 'old title', 'body' => 'old body']); + $post->_disableAutoRefresh(); $this->assertSame('old title', $post->getTitle()); $this->assertSame('old body', $post->getBody()); $post - ->withoutAutoRefresh(static function(Proxy $proxy): void { + ->_withoutAutoRefresh(static function(Proxy $proxy): void { $proxy - ->forceSet('title', 'new title') - ->forceSet('body', 'new body') + ->_set('title', 'new title') + ->_set('body', 'new body') ; }) - ->forceSet('title', 'another new title') - ->forceSet('body', 'another new body') - ->save() + ->_set('title', 'another new title') + ->_set('body', 'another new body') + ->_save() ; $this->assertSame('another new title', $post->getTitle()); @@ -269,27 +256,29 @@ public function without_auto_refresh_does_not_enable_auto_refresh_if_it_was_disa public function without_auto_refresh_keeps_disabled_if_originally_disabled(): void { $post = $this->postFactoryClass()::createOne(['title' => 'old title', 'body' => 'old body']); + $post->_disableAutoRefresh(); $this->assertSame('old title', $post->getTitle()); $this->assertSame('old body', $post->getBody()); $post - ->withoutAutoRefresh(static function(Proxy $proxy): void { + ->_withoutAutoRefresh(static function(Proxy $proxy): void { $proxy - ->forceSet('title', 'new title') - ->forceSet('body', 'new body') + ->_set('title', 'new title') + ->_set('body', 'new body') ; }) - ->save() - ->forceSet('title', 'another new title') - ->forceSet('body', 'another new body') - ->save() + ->_save() + ->_set('title', 'another new title') + ->_set('body', 'another new body') + ->_save() ; $this->assertSame('another new title', $post->getTitle()); $this->assertSame('another new body', $post->getBody()); } + /** @return class-string */ abstract protected function postFactoryClass(): string; abstract protected function postClass(): string; diff --git a/tests/Functional/RepositoryProxyTest.php b/tests/Functional/RepositoryDecoratorTest.php similarity index 80% rename from tests/Functional/RepositoryProxyTest.php rename to tests/Functional/RepositoryDecoratorTest.php index e184f0b64..fae07191a 100644 --- a/tests/Functional/RepositoryProxyTest.php +++ b/tests/Functional/RepositoryDecoratorTest.php @@ -15,17 +15,17 @@ use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; use Zenstruck\Assert; -use Zenstruck\Foundry\Proxy; +use Zenstruck\Foundry\Persistence\Proxy; use Zenstruck\Foundry\Test\Factories; use Zenstruck\Foundry\Test\ResetDatabase; use Zenstruck\Foundry\Tests\Fixtures\Entity\Category; -use function Zenstruck\Foundry\repository; +use function Zenstruck\Foundry\Persistence\proxy_repository; /** * @author Kevin Bond */ -abstract class RepositoryProxyTest extends KernelTestCase +abstract class RepositoryDecoratorTest extends KernelTestCase { use ExpectDeprecationTrait, Factories, ResetDatabase; @@ -34,7 +34,7 @@ abstract class RepositoryProxyTest extends KernelTestCase */ public function assertions(): void { - $repository = repository($this->categoryClass()); + $repository = proxy_repository($this->categoryClass()); $repository->assert()->empty(); @@ -96,14 +96,14 @@ public function assertions(): void */ public function assertions_legacy(): void { - $repository = repository($this->categoryClass()); + $repository = proxy_repository($this->categoryClass()); - $this->expectDeprecation('Since zenstruck\foundry 1.8.0: Using RepositoryProxy::assertEmpty() is deprecated, use RepositoryProxy::assert()->empty().'); - $this->expectDeprecation('Since zenstruck\foundry 1.8.0: Using RepositoryProxy::assertCount() is deprecated, use RepositoryProxy::assert()->count().'); - $this->expectDeprecation('Since zenstruck\foundry 1.8.0: Using RepositoryProxy::assertCountGreaterThan() is deprecated, use RepositoryProxy::assert()->countGreaterThan().'); - $this->expectDeprecation('Since zenstruck\foundry 1.8.0: Using RepositoryProxy::assertCountGreaterThanOrEqual() is deprecated, use RepositoryProxy::assert()->countGreaterThanOrEqual().'); - $this->expectDeprecation('Since zenstruck\foundry 1.8.0: Using RepositoryProxy::assertCountLessThan() is deprecated, use RepositoryProxy::assert()->countLessThan().'); - $this->expectDeprecation('Since zenstruck\foundry 1.8.0: Using RepositoryProxy::assertCountLessThanOrEqual() is deprecated, use RepositoryProxy::assert()->countLessThanOrEqual().'); + $this->expectDeprecation('Since zenstruck\foundry 1.8.0: Using RepositoryDecorator::assertEmpty() is deprecated, use RepositoryDecorator::assert()->empty().'); + $this->expectDeprecation('Since zenstruck\foundry 1.8.0: Using RepositoryDecorator::assertCount() is deprecated, use RepositoryDecorator::assert()->count().'); + $this->expectDeprecation('Since zenstruck\foundry 1.8.0: Using RepositoryDecorator::assertCountGreaterThan() is deprecated, use RepositoryDecorator::assert()->countGreaterThan().'); + $this->expectDeprecation('Since zenstruck\foundry 1.8.0: Using RepositoryDecorator::assertCountGreaterThanOrEqual() is deprecated, use RepositoryDecorator::assert()->countGreaterThanOrEqual().'); + $this->expectDeprecation('Since zenstruck\foundry 1.8.0: Using RepositoryDecorator::assertCountLessThan() is deprecated, use RepositoryDecorator::assert()->countLessThan().'); + $this->expectDeprecation('Since zenstruck\foundry 1.8.0: Using RepositoryDecorator::assertCountLessThanOrEqual() is deprecated, use RepositoryDecorator::assert()->countLessThanOrEqual().'); $repository->assertEmpty(); @@ -121,7 +121,7 @@ public function assertions_legacy(): void */ public function can_fetch_objects(): void { - $repository = repository($this->categoryClass()); + $repository = proxy_repository($this->categoryClass()); $this->categoryFactoryClass()::createMany(2); @@ -141,14 +141,14 @@ public function can_fetch_objects(): void */ public function find_can_be_passed_proxy_or_object_or_array(): void { - $repository = repository($this->categoryClass()); + $repository = proxy_repository($this->categoryClass()); $proxy = $this->categoryFactoryClass()::createOne(['name' => 'foo']); $this->assertInstanceOf(Proxy::class, $repository->find(['name' => 'foo'])); if (Category::class === $this->categoryClass()) { $this->assertInstanceOf(Proxy::class, $repository->find($proxy)); - $this->assertInstanceOf(Proxy::class, $repository->find($proxy->object())); + $this->assertInstanceOf(Proxy::class, $repository->find($proxy->_real())); } } @@ -162,7 +162,7 @@ public function can_find_random_object(): void $ids = []; while (5 !== \count(\array_unique($ids))) { - $ids[] = repository($this->categoryClass())->random()->getId(); + $ids[] = proxy_repository($this->categoryClass())->random()->getId(); } $this->assertCount(5, \array_unique($ids)); @@ -176,7 +176,7 @@ public function at_least_one_object_must_exist_to_get_random_object(): void $this->expectException(\RuntimeException::class); $this->expectExceptionMessage(\sprintf('At least 1 "%s" object(s) must have been persisted (0 persisted).', $this->categoryClass())); - repository($this->categoryClass())->random(); + proxy_repository($this->categoryClass())->random(); } /** @@ -186,7 +186,7 @@ public function can_find_random_set_of_objects(): void { $this->categoryFactoryClass()::createMany(5); - $objects = repository($this->categoryClass())->randomSet(3); + $objects = proxy_repository($this->categoryClass())->randomSet(3); $this->assertCount(3, $objects); $this->assertCount( @@ -208,7 +208,7 @@ public function random_set_number_must_be_positive(): void $this->expectException(\InvalidArgumentException::class); $this->expectExceptionMessage('$number must be positive (-1 given).'); - repository($this->categoryClass())->randomSet(-1); + proxy_repository($this->categoryClass())->randomSet(-1); } /** @@ -221,7 +221,7 @@ public function the_number_of_persisted_objects_must_be_at_least_the_random_set_ $this->expectException(\RuntimeException::class); $this->expectExceptionMessage(\sprintf('At least 2 "%s" object(s) must have been persisted (1 persisted).', $this->categoryClass())); - repository($this->categoryClass())->randomSet(2); + proxy_repository($this->categoryClass())->randomSet(2); } /** @@ -234,7 +234,7 @@ public function can_find_random_range_of_objects(): void $counts = []; while (4 !== \count(\array_unique($counts))) { - $counts[] = \count(repository($this->categoryClass())->randomRange(0, 3)); + $counts[] = \count(proxy_repository($this->categoryClass())->randomRange(0, 3)); } $this->assertCount(4, \array_unique($counts)); @@ -256,7 +256,7 @@ public function the_number_of_persisted_objects_must_be_at_least_the_random_rang $this->expectException(\RuntimeException::class); $this->expectExceptionMessage(\sprintf('At least 2 "%s" object(s) must have been persisted (1 persisted).', $this->categoryClass())); - repository($this->categoryClass())->randomRange(0, 2); + proxy_repository($this->categoryClass())->randomRange(0, 2); } /** @@ -267,7 +267,7 @@ public function random_range_min_cannot_be_less_than_zero(): void $this->expectException(\InvalidArgumentException::class); $this->expectExceptionMessage('$min must be positive (-1 given).'); - repository($this->categoryClass())->randomRange(-1, 3); + proxy_repository($this->categoryClass())->randomRange(-1, 3); } /** @@ -278,7 +278,7 @@ public function random_set_max_cannot_be_less_than_min(): void $this->expectException(\InvalidArgumentException::class); $this->expectExceptionMessage('$max (3) cannot be less than $min (5).'); - repository($this->categoryClass())->randomRange(5, 3); + proxy_repository($this->categoryClass())->randomRange(5, 3); } /** @@ -337,11 +337,24 @@ public function can_use_get_count(): void $categoryFactoryClass::createMany(4); - $this->expectDeprecation('Since zenstruck\foundry 1.5.0: Using RepositoryProxy::getCount() is deprecated, use RepositoryProxy::count() (it is now Countable).'); + $this->expectDeprecation('Since zenstruck\foundry 1.5.0: Using RepositoryDecorator::getCount() is deprecated, use RepositoryDecorator::count() (it is now Countable).'); $this->assertSame(4, $categoryFactoryClass::repository()->getCount()); } + /** + * @test + * @group legacy + */ + public function can_use_new_class_as_legacy_one(): void + { + self::assertTrue($this->categoryFactoryClass()::repository() instanceof \Zenstruck\Foundry\Persistence\RepositoryDecorator); + self::assertTrue($this->categoryFactoryClass()::repository() instanceof \Zenstruck\Foundry\RepositoryProxy); + + self::assertTrue($this->categoryFactoryClass()::assert() instanceof \Zenstruck\Foundry\Persistence\RepositoryAssertions); + self::assertTrue($this->categoryFactoryClass()::assert() instanceof \Zenstruck\Foundry\RepositoryAssertions); + } + abstract protected function categoryClass(): string; abstract protected function categoryFactoryClass(): string; diff --git a/tests/Functional/StoryTest.php b/tests/Functional/StoryTest.php index 99f1f51ca..c9476748d 100644 --- a/tests/Functional/StoryTest.php +++ b/tests/Functional/StoryTest.php @@ -13,7 +13,7 @@ use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; -use Zenstruck\Foundry\Proxy; +use Zenstruck\Foundry\Persistence\Proxy; use Zenstruck\Foundry\Test\Factories; use Zenstruck\Foundry\Test\ResetDatabase; use Zenstruck\Foundry\Tests\Fixtures\Entity\Category; @@ -233,8 +233,8 @@ public function story_can_access_its_own_pool(): void $item = StoryWhichReadsItsOwnPool::get('random-from-own-pool'); self::assertInstanceOf(Proxy::class, $item); - self::assertInstanceOf(Category::class, $item->object()); + self::assertInstanceOf(Category::class, $item->_real()); - self::assertContains($item->object()->getName(), ['php', 'symfony']); + self::assertContains($item->_real()->getName(), ['php', 'symfony']); } } diff --git a/tests/Functional/WithDoctrineDisabledKernelTest.php b/tests/Functional/WithDoctrineDisabledKernelTest.php index c3a7d55e8..c6ae3992c 100644 --- a/tests/Functional/WithDoctrineDisabledKernelTest.php +++ b/tests/Functional/WithDoctrineDisabledKernelTest.php @@ -37,11 +37,11 @@ public static function setUpBeforeClass(): void */ public function create_object(): void { - $address = AddressFactory::new()->withoutPersisting()->create(['value' => 'test'])->object(); + $address = AddressFactory::new()->withoutPersisting()->create(['value' => 'test']); Assert::that($address)->isInstanceOf(Address::class); Assert::that($address->getValue())->is('test'); - $address = AddressFactory::createOne(['value' => 'test'])->object(); + $address = AddressFactory::createOne(['value' => 'test']); Assert::that($address)->isInstanceOf(Address::class); Assert::that($address->getValue())->is('test'); } diff --git a/tests/Unit/Bundle/DependencyInjection/ZenstruckFoundryExtensionTest.php b/tests/Unit/Bundle/DependencyInjection/ZenstruckFoundryExtensionTest.php index f892213c9..7780f7940 100644 --- a/tests/Unit/Bundle/DependencyInjection/ZenstruckFoundryExtensionTest.php +++ b/tests/Unit/Bundle/DependencyInjection/ZenstruckFoundryExtensionTest.php @@ -18,7 +18,7 @@ use Symfony\Bundle\MakerBundle\MakerBundle; use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; use Zenstruck\Foundry\Bundle\DependencyInjection\ZenstruckFoundryExtension; -use Zenstruck\Foundry\Instantiator; +use Zenstruck\Foundry\Object\Instantiator; /** * @author Kevin Bond @@ -57,6 +57,12 @@ public function default_config(): void $this->assertContainerBuilderNotHasService('.zenstruck_foundry.maker.story'); $this->assertContainerBuilderHasServiceDefinitionWithTag('.zenstruck_foundry.maker.factory_stub', 'console.command'); $this->assertContainerBuilderHasServiceDefinitionWithTag('.zenstruck_foundry.maker.story_stub', 'console.command'); + + $configurationArguments = $this->container->getDefinition('.zenstruck_foundry.configuration')->getArguments(); + $this->assertSame(['default'], $configurationArguments['$ormConnectionsToReset']); + $this->assertSame(['default'], $configurationArguments['$ormObjectManagersToReset']); + $this->assertSame('schema', $configurationArguments['$ormResetMode']); + $this->assertSame(['default'], $configurationArguments['$odmObjectManagersToReset']); } /** @@ -118,15 +124,54 @@ public function cannot_set_faker_seed_and_service(): void */ public function custom_instantiator_config(): void { + $this->load([ + 'instantiator' => [ + 'use_constructor' => false, + 'allow_extra_attributes' => true, + 'always_force_properties' => true, + ], + ]); + + $this->assertContainerBuilderHasServiceDefinitionWithMethodCall('.zenstruck_foundry.default_instantiator', 'allowExtra'); + $this->assertContainerBuilderHasServiceDefinitionWithMethodCall('.zenstruck_foundry.default_instantiator', 'alwaysForce'); + + $instantiator = $this->container->get('.zenstruck_foundry.default_instantiator'); + + // matthiasnoback/symfony-dependency-injection-test cannot assert if a service is created through a factory. + // so, we're checking that private property "Instantiator::$withoutConstructor" was set to true. + $useConstructor = \Closure::bind(static fn(Instantiator $instantiator) => $instantiator->useConstructor, null, Instantiator::class)($instantiator); + self::assertFalse($useConstructor); + } + + /** + * @test + * @group legacy + */ + public function can_configure_instantiator_without_constructor(): void + { + $this->load(['instantiator' => ['without_constructor' => true]]); + + $instantiator = $this->container->get('.zenstruck_foundry.default_instantiator'); + + // matthiasnoback/symfony-dependency-injection-test cannot assert if a service is created through a factory. + // so, we're checking that private property "Instantiator::$withoutConstructor" was set to true. + $useConstructor = \Closure::bind(static fn(Instantiator $instantiator) => $instantiator->useConstructor, null, Instantiator::class)($instantiator); + self::assertFalse($useConstructor); + } + + /** + * @test + * @group legacy + */ + public function throws_exception_when_instantiator_has_wrong_configuration(): void + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Cannot set "without_constructor" and "use_constructor" to the same value.'); + $this->load(['instantiator' => [ 'without_constructor' => true, - 'allow_extra_attributes' => true, - 'always_force_properties' => true, + 'use_constructor' => true, ]]); - - $this->assertContainerBuilderHasServiceDefinitionWithMethodCall('.zenstruck_foundry.default_instantiator', 'withoutConstructor'); - $this->assertContainerBuilderHasServiceDefinitionWithMethodCall('.zenstruck_foundry.default_instantiator', 'allowExtraAttributes'); - $this->assertContainerBuilderHasServiceDefinitionWithMethodCall('.zenstruck_foundry.default_instantiator', 'alwaysForceProperties'); } /** @@ -157,7 +202,7 @@ public function cannot_configure_allow_extra_attributes_if_using_custom_instanti public function cannot_configure_without_constructor_if_using_custom_instantiator_service(): void { $this->expectException(InvalidConfigurationException::class); - $this->expectExceptionMessage('Invalid configuration for path "zenstruck_foundry.instantiator": Cannot set "without_constructor" when using custom service.'); + $this->expectExceptionMessage('Invalid configuration for path "zenstruck_foundry.instantiator": Cannot set "use_constructor: false" when using custom service.'); $this->load(['instantiator' => ['service' => 'my_instantiator', 'without_constructor' => true]]); } @@ -175,6 +220,7 @@ public function cannot_configure_always_force_properties_if_using_custom_instant /** * @test + * @group legacy */ public function can_enable_auto_refresh_proxies(): void { @@ -187,6 +233,7 @@ public function can_enable_auto_refresh_proxies(): void /** * @test + * @group legacy */ public function can_disable_auto_refresh_proxies(): void { @@ -211,21 +258,36 @@ public function it_registers_makers_if_maker_bundle_enabled(): void /** * @test + * @group legacy * @testWith ["orm"] * ["odm"] */ - public function cannot_configure_database_resetter_if_doctrine_not_enabled(string $doctrine): void + public function cannot_configure_legacy_database_resetter_if_doctrine_not_enabled(string $doctrine): void { $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessage(\sprintf('should be enabled to use config under "database_resetter.%s"', $doctrine)); + $this->expectExceptionMessage(\sprintf('should be enabled to use config under "%s.reset"', 'orm' === $doctrine ? 'orm' : 'mongo')); $this->load(['database_resetter' => [$doctrine => []]]); } /** * @test + * @testWith ["orm"] + * ["mongo"] + */ + public function cannot_configure_database_resetter_if_doctrine_not_enabled(string $doctrine): void + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage(\sprintf('should be enabled to use config under "%s.reset"', $doctrine)); + + $this->load([$doctrine => ['reset' => []]]); + } + + /** + * @test + * @group legacy */ - public function can_configure_database_resetter_if_doctrine_enabled(): void + public function can_configure_legacy_database_resetter_if_doctrine_enabled(): void { $this->setParameter('kernel.bundles', [DoctrineBundle::class, DoctrineMongoDBBundle::class]); @@ -243,6 +305,25 @@ public function can_configure_database_resetter_if_doctrine_enabled(): void $this->assertSame(['object_manager_odm'], $configurationArguments['$odmObjectManagersToReset']); } + /** + * @test + */ + public function can_configure_database_resetter_if_doctrine_enabled(): void + { + $this->setParameter('kernel.bundles', [DoctrineBundle::class, DoctrineMongoDBBundle::class]); + + $this->load([ + 'orm' => ['reset' => ['connections' => ['orm_connection'], 'entity_managers' => ['object_manager_orm'], 'mode' => 'migrate']], + 'mongo' => ['reset' => ['document_managers' => ['object_manager_odm']]], + ]); + + $configurationArguments = $this->container->getDefinition('.zenstruck_foundry.configuration')->getArguments(); + $this->assertSame(['orm_connection'], $configurationArguments['$ormConnectionsToReset']); + $this->assertSame(['object_manager_orm'], $configurationArguments['$ormObjectManagersToReset']); + $this->assertSame('migrate', $configurationArguments['$ormResetMode']); + $this->assertSame(['object_manager_odm'], $configurationArguments['$odmObjectManagersToReset']); + } + /** * @return ZenstruckFoundryExtension[] */ diff --git a/tests/Unit/FactoryCollectionTest.php b/tests/Unit/FactoryCollectionTest.php index 9a3205df5..b4a8a2680 100644 --- a/tests/Unit/FactoryCollectionTest.php +++ b/tests/Unit/FactoryCollectionTest.php @@ -16,7 +16,7 @@ use Zenstruck\Foundry\Test\Factories; use Zenstruck\Foundry\Tests\Fixtures\Entity\Category; -use function Zenstruck\Foundry\anonymous; +use function Zenstruck\Foundry\Persistence\persistent_factory; /** * @author Kevin Bond @@ -30,7 +30,7 @@ final class FactoryCollectionTest extends TestCase */ public function can_create_with_static_size(): void { - $collection = FactoryCollection::set(anonymous(Category::class), 2); + $collection = FactoryCollection::many(persistent_factory(Category::class), 2); $this->assertCount(2, $collection->create()); $this->assertCount(2, $collection->create()); @@ -44,7 +44,7 @@ public function can_create_with_static_size(): void */ public function can_create_with_random_range(): void { - $collection = FactoryCollection::range(anonymous(Category::class), 0, 3); + $collection = FactoryCollection::range(persistent_factory(Category::class), 0, 3); $counts = []; while (4 !== \count(\array_unique($counts))) { @@ -68,7 +68,7 @@ public function min_must_be_less_than_or_equal_to_max(): void $this->expectException(\InvalidArgumentException::class); $this->expectExceptionMessage('Min must be less than max.'); - FactoryCollection::range(anonymous(Category::class), 4, 3); + FactoryCollection::range(persistent_factory(Category::class), 4, 3); } /** @@ -76,7 +76,7 @@ public function min_must_be_less_than_or_equal_to_max(): void */ public function can_create_with_sequence(): void { - $collection = FactoryCollection::sequence(anonymous(Category::class), [['name' => 'foo'], ['name' => 'bar']]); + $collection = FactoryCollection::sequence(persistent_factory(Category::class), [['name' => 'foo'], ['name' => 'bar']]); $categories = $collection->create(); $this->assertCount(2, $categories); diff --git a/tests/Unit/FactoryTest.php b/tests/Unit/FactoryTest.php index 1e8dba946..bedcd1ccd 100644 --- a/tests/Unit/FactoryTest.php +++ b/tests/Unit/FactoryTest.php @@ -18,13 +18,17 @@ use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; use Zenstruck\Foundry\Factory; use Zenstruck\Foundry\LazyValue; -use Zenstruck\Foundry\Proxy; +use Zenstruck\Foundry\Persistence\Proxy; +use Zenstruck\Foundry\Proxy as ProxyObject; use Zenstruck\Foundry\Test\Factories; use Zenstruck\Foundry\Tests\Fixtures\Entity\Category; use Zenstruck\Foundry\Tests\Fixtures\Entity\Post; +use Zenstruck\Foundry\Tests\Fixtures\Factories\LegacyPostFactory; -use function Zenstruck\Foundry\anonymous; +use function Zenstruck\Foundry\faker; use function Zenstruck\Foundry\lazy; +use function Zenstruck\Foundry\Persistence\persistent_factory; +use function Zenstruck\Foundry\Persistence\proxy_factory; /** * @author Kevin Bond @@ -42,15 +46,24 @@ public function can_instantiate_object(): void $attributeCallback = static fn(): array => ['title' => 'title', 'body' => 'body']; $attributeArrayWithLazyValue = ['title' => lazy(fn() => 'title'), 'body' => 'body']; - $this->assertSame('title', anonymous(Post::class, $attributeArray)->create()->getTitle()); - $this->assertSame('title', anonymous(Post::class)->create($attributeArray)->getTitle()); - $this->assertSame('title', anonymous(Post::class)->withAttributes($attributeArray)->create()->getTitle()); - $this->assertSame('title', anonymous(Post::class, $attributeCallback)->create()->getTitle()); - $this->assertSame('title', anonymous(Post::class)->create($attributeCallback)->getTitle()); - $this->assertSame('title', anonymous(Post::class)->withAttributes($attributeCallback)->create()->getTitle()); - $this->assertSame('title', anonymous(Post::class, $attributeArrayWithLazyValue)->create()->getTitle()); - $this->assertSame('title', anonymous(Post::class)->create($attributeArrayWithLazyValue)->getTitle()); - $this->assertSame('title', anonymous(Post::class)->withAttributes($attributeArrayWithLazyValue)->create()->getTitle()); + $this->assertSame('title', persistent_factory(Post::class, $attributeArray)->create()->getTitle()); + $this->assertSame('title', persistent_factory(Post::class)->create($attributeArray)->getTitle()); + $this->assertSame('title', persistent_factory(Post::class)->with($attributeArray)->create()->getTitle()); + $this->assertSame('title', persistent_factory(Post::class, $attributeCallback)->create()->getTitle()); + $this->assertSame('title', persistent_factory(Post::class)->create($attributeCallback)->getTitle()); + $this->assertSame('title', persistent_factory(Post::class)->with($attributeCallback)->create()->getTitle()); + $this->assertSame('title', persistent_factory(Post::class, $attributeArrayWithLazyValue)->create()->getTitle()); + $this->assertSame('title', persistent_factory(Post::class)->create($attributeArrayWithLazyValue)->getTitle()); + $this->assertSame('title', persistent_factory(Post::class)->with($attributeArrayWithLazyValue)->create()->getTitle()); + } + + /** + * @test + * @group legacy + */ + public function can_use_legacy_with_attributes(): void + { + $this->assertSame('title', persistent_factory(Post::class)->withAttributes(['title' => 'title', 'body' => 'body'])->create()->getTitle()); } /** @@ -64,11 +77,11 @@ public function lazy_values_are_only_calculated_if_needed(): void return 'title'; }); - $factory = anonymous(Post::class, ['title' => $lazyValue, 'body' => 'body']); + $factory = persistent_factory(Post::class, ['title' => $lazyValue, 'body' => 'body']); $post = $factory - ->withAttributes(['title' => $lazyValue]) - ->withAttributes(['title' => $lazyValue]) + ->with(['title' => $lazyValue]) + ->with(['title' => $lazyValue]) ->create(['title' => 'title']) ; @@ -76,8 +89,8 @@ public function lazy_values_are_only_calculated_if_needed(): void $this->assertSame(0, $count); $post = $factory - ->withAttributes(['title' => $lazyValue]) - ->withAttributes(['title' => $lazyValue]) + ->with(['title' => $lazyValue]) + ->with(['title' => $lazyValue]) ->create(['title' => $lazyValue]) ; @@ -96,7 +109,7 @@ public function lazy_memoized_values_are_only_calculated_once(): void return 'title'; }); - $factory = anonymous(Post::class, ['title' => $lazyValue, 'body' => 'body']); + $factory = persistent_factory(Post::class, ['title' => $lazyValue, 'body' => 'body']); $posts = $factory ->many(3) @@ -133,7 +146,7 @@ public function can_instantiate_many_objects_legacy(): void $this->assertSame('title', $objects[1]->getTitle()); $this->assertSame('title', $objects[2]->getTitle()); - $objects = (new Factory(Post::class))->withAttributes($attributeArray)->createMany(3); + $objects = (new Factory(Post::class))->with($attributeArray)->createMany(3); $this->assertCount(3, $objects); $this->assertSame('title', $objects[0]->getTitle()); @@ -154,7 +167,7 @@ public function can_instantiate_many_objects_legacy(): void $this->assertSame('title', $objects[1]->getTitle()); $this->assertSame('title', $objects[2]->getTitle()); - $objects = (new Factory(Post::class))->withAttributes($attributeCallback)->createMany(3); + $objects = (new Factory(Post::class))->with($attributeCallback)->createMany(3); $this->assertCount(3, $objects); $this->assertSame('title', $objects[0]->getTitle()); @@ -169,7 +182,7 @@ public function can_set_instantiator(): void { $attributeArray = ['title' => 'original title', 'body' => 'original body']; - $object = anonymous(Post::class) + $object = persistent_factory(Post::class) ->instantiateWith(function(array $attributes, string $class) use ($attributeArray): Post { $this->assertSame(Post::class, $class); $this->assertSame($attributes, $attributeArray); @@ -190,7 +203,7 @@ public function can_add_before_instantiate_events(): void { $attributeArray = ['title' => 'original title', 'body' => 'original body']; - $object = anonymous(Post::class) + $object = persistent_factory(Post::class) ->beforeInstantiate(static function(array $attributes): array { $attributes['title'] = 'title'; @@ -216,7 +229,7 @@ public function before_instantiate_event_must_return_an_array(): void $this->expectException(\LogicException::class); $this->expectExceptionMessage('Before Instantiate event callback must return an array.'); - anonymous(Post::class)->beforeInstantiate(static function(): void {})->create(); + persistent_factory(Post::class)->beforeInstantiate(static function(): void {})->create(); } /** @@ -226,7 +239,7 @@ public function can_add_after_instantiate_events(): void { $attributesArray = ['title' => 'title', 'body' => 'body']; - $object = anonymous(Post::class) + $object = persistent_factory(Post::class) ->afterInstantiate(function(Post $post, array $attributes) use ($attributesArray): void { $this->assertSame($attributesArray, $attributes); @@ -248,10 +261,10 @@ public function can_add_after_instantiate_events(): void */ public function can_register_custom_faker(): void { - $defaultFaker = Factory::faker(); + $defaultFaker = faker(); Factory::configuration()->setFaker(Faker\Factory::create()); - $this->assertNotSame(\spl_object_id(Factory::faker()), \spl_object_id($defaultFaker)); + $this->assertNotSame(\spl_object_id(faker()), \spl_object_id($defaultFaker)); } /** @@ -261,7 +274,7 @@ public function can_register_default_instantiator(): void { Factory::configuration()->setInstantiator(static fn(): Post => new Post('different title', 'different body')); - $object = anonymous(Post::class, ['title' => 'title', 'body' => 'body'])->create(); + $object = persistent_factory(Post::class, ['title' => 'title', 'body' => 'body'])->create(); $this->assertSame('different title', $object->getTitle()); $this->assertSame('different body', $object->getBody()); @@ -272,10 +285,10 @@ public function can_register_default_instantiator(): void */ public function instantiating_with_proxy_attribute_normalizes_to_underlying_object(): void { - $object = anonymous(Post::class)->create([ + $object = persistent_factory(Post::class)->create([ 'title' => 'title', 'body' => 'body', - 'category' => new Proxy(new Category()), + 'category' => new ProxyObject(new Category()), ]); $this->assertInstanceOf(Category::class, $object->getCategory()); @@ -286,10 +299,10 @@ public function instantiating_with_proxy_attribute_normalizes_to_underlying_obje */ public function instantiating_with_factory_attribute_instantiates_the_factory(): void { - $object = anonymous(Post::class)->create([ + $object = persistent_factory(Post::class)->create([ 'title' => 'title', 'body' => 'body', - 'category' => anonymous(Category::class), + 'category' => persistent_factory(Category::class), ]); $this->assertInstanceOf(Category::class, $object->getCategory()); @@ -300,10 +313,10 @@ public function instantiating_with_factory_attribute_instantiates_the_factory(): */ public function factory_is_immutable(): void { - $factory = anonymous(Post::class); + $factory = persistent_factory(Post::class); $objectId = \spl_object_id($factory); - $this->assertNotSame(\spl_object_id($factory->withAttributes([])), $objectId); + $this->assertNotSame(\spl_object_id($factory->with([])), $objectId); $this->assertNotSame(\spl_object_id($factory->withoutPersisting()), $objectId); $this->assertNotSame(\spl_object_id($factory->instantiateWith(static function(): void {})), $objectId); $this->assertNotSame(\spl_object_id($factory->beforeInstantiate(static function(): void {})), $objectId); @@ -313,6 +326,7 @@ public function factory_is_immutable(): void /** * @test + * @group legacy */ public function can_create_object(): void { @@ -325,9 +339,9 @@ public function can_create_object(): void Factory::configuration()->setManagerRegistry($registry)->disableDefaultProxyAutoRefresh(); - $object = anonymous(Post::class)->create(['title' => 'title', 'body' => 'body']); + $object = persistent_factory(Post::class)->create(['title' => 'title', 'body' => 'body']); - $this->assertInstanceOf(Proxy::class, $object); + $this->assertInstanceOf(Post::class, $object); $this->assertSame('title', $object->getTitle()); } @@ -349,9 +363,9 @@ public function can_create_many_objects_legacy(): void $objects = (new Factory(Post::class))->createMany(3, ['title' => 'title', 'body' => 'body']); $this->assertCount(3, $objects); - $this->assertInstanceOf(Proxy::class, $objects[0]); - $this->assertInstanceOf(Proxy::class, $objects[1]); - $this->assertInstanceOf(Proxy::class, $objects[2]); + $this->assertInstanceOf(Post::class, $objects[0]); + $this->assertInstanceOf(Post::class, $objects[1]); + $this->assertInstanceOf(Post::class, $objects[2]); $this->assertSame('title', $objects[0]->getTitle()); $this->assertSame('title', $objects[1]->getTitle()); $this->assertSame('title', $objects[2]->getTitle()); @@ -359,6 +373,7 @@ public function can_create_many_objects_legacy(): void /** * @test + * @group legacy */ public function can_add_after_persist_events(): void { @@ -374,7 +389,7 @@ public function can_add_after_persist_events(): void $expectedAttributes = ['shortDescription' => 'short desc', 'title' => 'title', 'body' => 'body']; $calls = 0; - $object = anonymous(Post::class, ['shortDescription' => 'short desc']) + $object = persistent_factory(Post::class, ['shortDescription' => 'short desc']) ->afterPersist(function(Proxy $post, array $attributes) use ($expectedAttributes, &$calls): void { /* @var Post $post */ $this->assertSame($expectedAttributes, $attributes); @@ -417,7 +432,7 @@ public function trying_to_persist_without_manager_registry_throws_exception(): v $this->expectException(\RuntimeException::class); $this->expectExceptionMessage('Foundry was booted without doctrine. Ensure your TestCase extends '.KernelTestCase::class); - anonymous(Post::class)->create(['title' => 'title', 'body' => 'body'])->save(); + proxy_factory(Post::class)->create(['title' => 'title', 'body' => 'body'])->_save(); } /** @@ -429,8 +444,30 @@ public function can_use_arrays_for_attribute_values(): void public $value; }; - $factory = anonymous($object::class)->create(['value' => ['foo' => 'bar']]); + $factory = persistent_factory($object::class)->create(['value' => ['foo' => 'bar']]); $this->assertSame(['foo' => 'bar'], $factory->value); } + + /** + * @test + * @group legacy + */ + public function can_use_legacy_model_factory(): void + { + $post = LegacyPostFactory::createOne(['title' => 'title', 'body' => 'body']); + + self::assertSame('title', $post->getTitle()); + self::assertSame('body', $post->getBody()); + } + + /** + * @test + * @group legacy + */ + public function can_use_legacy_proxy_class(): void + { + $post = proxy_factory(Post::class)->create(['title' => 'title', 'body' => 'body']); + self::assertInstanceOf(ProxyObject::class, $post); + } } diff --git a/tests/Unit/FunctionsTest.php b/tests/Unit/FunctionsTest.php index 0526d0c4c..5a8b4dff2 100644 --- a/tests/Unit/FunctionsTest.php +++ b/tests/Unit/FunctionsTest.php @@ -17,8 +17,8 @@ use PHPUnit\Framework\TestCase; use Zenstruck\Foundry\Factory; use Zenstruck\Foundry\LazyValue; -use Zenstruck\Foundry\Proxy; -use Zenstruck\Foundry\RepositoryProxy; +use Zenstruck\Foundry\Persistence\Proxy; +use Zenstruck\Foundry\Persistence\RepositoryDecorator; use Zenstruck\Foundry\Test\Factories; use Zenstruck\Foundry\Tests\Fixtures\Entity\Category; use Zenstruck\Foundry\Tests\Fixtures\Entity\Post; @@ -26,10 +26,12 @@ use function Zenstruck\Foundry\create; use function Zenstruck\Foundry\create_many; use function Zenstruck\Foundry\faker; -use function Zenstruck\Foundry\instantiate; use function Zenstruck\Foundry\instantiate_many; use function Zenstruck\Foundry\lazy; use function Zenstruck\Foundry\memoize; +use function Zenstruck\Foundry\object; +use function Zenstruck\Foundry\Persistence\disable_persisting; +use function Zenstruck\Foundry\Persistence\enable_persisting; use function Zenstruck\Foundry\repository; /** @@ -74,27 +76,27 @@ public function memoize(): void */ public function instantiate(): void { - $proxy = instantiate(Post::class, ['title' => 'title', 'body' => 'body']); + $object = object(Post::class, ['title' => 'title', 'body' => 'body']); - $this->assertInstanceOf(Post::class, $proxy->object()); - $this->assertFalse($proxy->isPersisted()); - $this->assertSame('title', $proxy->getTitle()); + $this->assertInstanceOf(Post::class, $object); + $this->assertSame('title', $object->getTitle()); } /** * @test + * @group legacy */ public function instantiate_many(): void { $objects = instantiate_many(3, Category::class); $this->assertCount(3, $objects); - $this->assertInstanceOf(Category::class, $objects[0]->object()); - $this->assertFalse($objects[0]->isPersisted()); + $this->assertInstanceOf(Category::class, $objects[0]->_real()); } /** * @test + * @group legacy */ public function create(): void { @@ -114,6 +116,7 @@ public function create(): void /** * @test + * @group legacy */ public function create_many(): void { @@ -133,6 +136,7 @@ public function create_many(): void /** * @test + * @group legacy */ public function repository(): void { @@ -146,6 +150,18 @@ public function repository(): void Factory::configuration()->setManagerRegistry($registry); - $this->assertInstanceOf(RepositoryProxy::class, repository(new Category())); + $this->assertInstanceOf(RepositoryDecorator::class, repository(new Category())); + } + + /** + * @test + * @group legacy + */ + public function enable_or_disable_persisting_can_be_called_without_doctrine(): void + { + $this->expectNotToPerformAssertions(); + + enable_persisting(); + disable_persisting(); } } diff --git a/tests/Unit/InstantiatorTest.php b/tests/Unit/InstantiatorTest.php index 24c9fd052..87a875376 100644 --- a/tests/Unit/InstantiatorTest.php +++ b/tests/Unit/InstantiatorTest.php @@ -13,7 +13,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; -use Zenstruck\Foundry\Instantiator; +use Zenstruck\Foundry\Object\Instantiator; /** * @author Kevin Bond @@ -27,7 +27,7 @@ final class InstantiatorTest extends TestCase */ public function default_instantiate(): void { - $object = (new Instantiator())([ + $object = Instantiator::withConstructor()([ 'propA' => 'A', 'propB' => 'B', 'propC' => 'C', @@ -49,7 +49,7 @@ public function can_use_snake_case_attributes(): void { $this->expectDeprecation('Since zenstruck\foundry 1.5.0: Using a differently cased attribute is deprecated, use the same case as the object property instead.'); - $object = (new Instantiator())([ + $object = Instantiator::withConstructor()([ 'prop_a' => 'A', 'prop_b' => 'B', 'prop_c' => 'C', @@ -71,7 +71,7 @@ public function can_use_kebab_case_attributes(): void { $this->expectDeprecation('Since zenstruck\foundry 1.5.0: Using a differently cased attribute is deprecated, use the same case as the object property instead.'); - $object = (new Instantiator())([ + $object = Instantiator::withConstructor()([ 'prop-a' => 'A', 'prop-b' => 'B', 'prop-c' => 'C', @@ -90,7 +90,7 @@ public function can_use_kebab_case_attributes(): void */ public function can_leave_off_default_constructor_argument(): void { - $object = (new Instantiator())([ + $object = Instantiator::withConstructor()([ 'propB' => 'B', ], InstantiatorDummy::class); @@ -103,7 +103,27 @@ public function can_leave_off_default_constructor_argument(): void */ public function can_instantiate_object_with_private_constructor(): void { - $object = (new Instantiator())([ + $object = Instantiator::withoutConstructor()([ + 'propA' => 'A', + 'propB' => 'B', + 'propC' => 'C', + 'propD' => 'D', + ], PrivateConstructorInstantiatorDummy::class); + + $this->assertSame('A', $object->propA); + $this->assertSame('A', $object->getPropA()); + $this->assertSame('setter B', $object->getPropB()); + $this->assertSame('setter C', $object->getPropC()); + $this->assertSame('setter D', $object->getPropD()); + } + + /** + * @test + * @group legacy + */ + public function can_instantiate_object_with_private_constructor_and_instantiator_configured_without_constructor(): void + { + $object = Instantiator::withConstructor()([ 'propA' => 'A', 'propB' => 'B', 'propC' => 'C', @@ -125,7 +145,7 @@ public function missing_constructor_argument_throws_exception(): void $this->expectException(\InvalidArgumentException::class); $this->expectExceptionMessage('Missing constructor argument "propB" for "Zenstruck\Foundry\Tests\Unit\InstantiatorDummy".'); - (new Instantiator())([], InstantiatorDummy::class); + Instantiator::withConstructor()([], InstantiatorDummy::class); } /** @@ -136,18 +156,46 @@ public function extra_attributes_throws_exception(): void $this->expectException(\InvalidArgumentException::class); $this->expectExceptionMessage('Cannot set attribute "extra" for object "Zenstruck\Foundry\Tests\Unit\InstantiatorDummy" (not public and no setter).'); - (new Instantiator())([ + Instantiator::withConstructor()([ 'propB' => 'B', 'extra' => 'foo', ], InstantiatorDummy::class); } + /** + * @test + * @group legacy + */ + public function can_set_attributes_that_should_be_optional_legacy(): void + { + $object = Instantiator::withConstructor()->allowExtraAttributes(['extra'])([ + 'propB' => 'B', + 'extra' => 'foo', + ], InstantiatorDummy::class); + + $this->assertSame('constructor B', $object->getPropB()); + } + + /** + * @test + * @group legacy + */ + public function can_always_allow_extra_attributes_legacy(): void + { + $object = Instantiator::withConstructor()->allowExtraAttributes()([ + 'propB' => 'B', + 'extra' => 'foo', + ], InstantiatorDummy::class); + + $this->assertSame('constructor B', $object->getPropB()); + } + /** * @test */ public function can_set_attributes_that_should_be_optional(): void { - $object = (new Instantiator())->allowExtraAttributes(['extra'])([ + $object = Instantiator::withConstructor()->allowExtra('extra')([ 'propB' => 'B', 'extra' => 'foo', ], InstantiatorDummy::class); @@ -163,7 +211,7 @@ public function extra_attributes_not_defined_throws_exception(): void $this->expectException(\InvalidArgumentException::class); $this->expectExceptionMessage('Cannot set attribute "extra2" for object "Zenstruck\Foundry\Tests\Unit\InstantiatorDummy" (not public and no setter).'); - (new Instantiator())->allowExtraAttributes(['extra1'])([ + Instantiator::withConstructor()->allowExtra('extra1')([ 'propB' => 'B', 'extra1' => 'foo', 'extra2' => 'bar', @@ -176,9 +224,9 @@ public function extra_attributes_not_defined_throws_exception(): void */ public function can_prefix_extra_attribute_key_with_optional_to_avoid_exception(): void { - $this->expectDeprecation('Since zenstruck\foundry 1.5.0: Using "optional:" attribute prefixes is deprecated, use Instantiator::allowExtraAttributes() instead (https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#instantiation).'); + $this->expectDeprecation('Since zenstruck\foundry 1.5.0: Using "optional:" attribute prefixes is deprecated, use Instantiator::allowExtra() instead (https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#instantiation).'); - $object = (new Instantiator())([ + $object = Instantiator::withConstructor()([ 'propB' => 'B', 'optional:extra' => 'foo', ], InstantiatorDummy::class); @@ -191,7 +239,7 @@ public function can_prefix_extra_attribute_key_with_optional_to_avoid_exception( */ public function can_always_allow_extra_attributes(): void { - $object = (new Instantiator())->allowExtraAttributes()([ + $object = Instantiator::withConstructor()->allowExtra()([ 'propB' => 'B', 'extra' => 'foo', ], InstantiatorDummy::class); @@ -204,7 +252,7 @@ public function can_always_allow_extra_attributes(): void */ public function can_disable_constructor(): void { - $object = (new Instantiator())->withoutConstructor()([ + $object = (Instantiator::withoutConstructor())([ 'propA' => 'A', 'propB' => 'B', 'propC' => 'C', @@ -218,12 +266,27 @@ public function can_disable_constructor(): void $this->assertSame('setter D', $object->getPropD()); } + /** + * @test + * @group legacy + */ + public function can_set_attributes_that_should_be_force_set_legacy(): void + { + $object = Instantiator::withoutConstructor()->alwaysForceProperties(['propD'])([ + 'propB' => 'B', + 'propD' => 'D', + ], InstantiatorDummy::class); + + $this->assertSame('setter B', $object->getPropB()); + $this->assertSame('D', $object->getPropD()); + } + /** * @test */ public function can_set_attributes_that_should_be_force_set(): void { - $object = (new Instantiator())->withoutConstructor()->alwaysForceProperties(['propD'])([ + $object = Instantiator::withoutConstructor()->alwaysForce('propD')([ 'propB' => 'B', 'propD' => 'D', ], InstantiatorDummy::class); @@ -232,15 +295,35 @@ public function can_set_attributes_that_should_be_force_set(): void $this->assertSame('D', $object->getPropD()); } + /** + * @test + * @group legacy + */ + public function can_disable_constructor_legacy(): void + { + $object = Instantiator::withConstructor()->withoutConstructor()([ + 'propA' => 'A', + 'propB' => 'B', + 'propC' => 'C', + 'propD' => 'D', + ], InstantiatorDummy::class); + + $this->assertSame('A', $object->propA); + $this->assertSame('A', $object->getPropA()); + $this->assertSame('setter B', $object->getPropB()); + $this->assertSame('setter C', $object->getPropC()); + $this->assertSame('setter D', $object->getPropD()); + } + /** * @test * @group legacy */ public function prefixing_attribute_key_with_force_sets_the_property_directly(): void { - $this->expectDeprecation('Since zenstruck\foundry 1.5.0: Using "force:" property prefixes is deprecated, use Instantiator::alwaysForceProperties() instead (https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#instantiation).'); + $this->expectDeprecation('Since zenstruck\foundry 1.5.0: Using "force:" property prefixes is deprecated, use Instantiator::alwaysForce() instead (https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#instantiation).'); - $object = (new Instantiator())([ + $object = Instantiator::withConstructor()([ 'propA' => 'A', 'propB' => 'B', 'propC' => 'C', @@ -260,9 +343,9 @@ public function prefixing_attribute_key_with_force_sets_the_property_directly(): */ public function prefixing_snake_case_attribute_key_with_force_sets_the_property_directly(): void { - $this->expectDeprecation('Since zenstruck\foundry 1.5.0: Using "force:" property prefixes is deprecated, use Instantiator::alwaysForceProperties() instead (https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#instantiation).'); + $this->expectDeprecation('Since zenstruck\foundry 1.5.0: Using "force:" property prefixes is deprecated, use Instantiator::alwaysForce() instead (https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#instantiation).'); - $object = (new Instantiator())([ + $object = Instantiator::withConstructor()([ 'prop_a' => 'A', 'prop_b' => 'B', 'prop_c' => 'C', @@ -282,9 +365,9 @@ public function prefixing_snake_case_attribute_key_with_force_sets_the_property_ */ public function prefixing_kebab_case_attribute_key_with_force_sets_the_property_directly(): void { - $this->expectDeprecation('Since zenstruck\foundry 1.5.0: Using "force:" property prefixes is deprecated, use Instantiator::alwaysForceProperties() instead (https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#instantiation).'); + $this->expectDeprecation('Since zenstruck\foundry 1.5.0: Using "force:" property prefixes is deprecated, use Instantiator::alwaysForce() instead (https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#instantiation).'); - $object = (new Instantiator())([ + $object = Instantiator::withConstructor()([ 'prop-a' => 'A', 'prop-b' => 'B', 'prop-c' => 'C', @@ -304,11 +387,11 @@ public function prefixing_kebab_case_attribute_key_with_force_sets_the_property_ */ public function prefixing_invalid_attribute_key_with_force_throws_exception(): void { - $this->expectDeprecation('Since zenstruck\foundry 1.5.0: Using "force:" property prefixes is deprecated, use Instantiator::alwaysForceProperties() instead (https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#instantiation).'); + $this->expectDeprecation('Since zenstruck\foundry 1.5.0: Using "force:" property prefixes is deprecated, use Instantiator::alwaysForce() instead (https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#instantiation).'); $this->expectException(\InvalidArgumentException::class); $this->expectExceptionMessage('Class "Zenstruck\Foundry\Tests\Unit\InstantiatorDummy" does not have property "extra".'); - (new Instantiator())([ + Instantiator::withConstructor()([ 'propB' => 'B', 'force:extra' => 'foo', ], InstantiatorDummy::class); @@ -316,6 +399,7 @@ public function prefixing_invalid_attribute_key_with_force_throws_exception(): v /** * @test + * @group legacy */ public function can_use_force_set_and_get(): void { @@ -374,6 +458,7 @@ public function force_set_throws_exception_for_invalid_property(): void /** * @test + * @group legacy */ public function force_get_throws_exception_for_invalid_property(): void { @@ -383,12 +468,32 @@ public function force_get_throws_exception_for_invalid_property(): void Instantiator::forceGet(new InstantiatorDummy('B'), 'invalid'); } + /** + * @test + * @group legacy + */ + public function can_use_always_force_mode_legacy(): void + { + $object = Instantiator::withConstructor()->alwaysForceProperties()([ + 'propA' => 'A', + 'propB' => 'B', + 'propC' => 'C', + 'propD' => 'D', + ], InstantiatorDummy::class); + + $this->assertSame('A', $object->propA); + $this->assertSame('A', $object->getPropA()); + $this->assertSame('constructor B', $object->getPropB()); + $this->assertSame('constructor C', $object->getPropC()); + $this->assertSame('D', $object->getPropD()); + } + /** * @test */ public function can_use_always_force_mode(): void { - $object = (new Instantiator())->alwaysForceProperties()([ + $object = Instantiator::withConstructor()->alwaysForce()([ 'propA' => 'A', 'propB' => 'B', 'propC' => 'C', @@ -410,7 +515,7 @@ public function can_use_always_force_mode_allows_snake_case(): void { $this->expectDeprecation('Since zenstruck\foundry 1.5.0: Using a differently cased attribute is deprecated, use the same case as the object property instead.'); - $object = (new Instantiator())->alwaysForceProperties()([ + $object = Instantiator::withConstructor()->alwaysForceProperties()([ 'prop_a' => 'A', 'prop_b' => 'B', 'prop_c' => 'C', @@ -432,7 +537,7 @@ public function can_use_always_force_mode_allows_kebab_case(): void { $this->expectDeprecation('Since zenstruck\foundry 1.5.0: Using a differently cased attribute is deprecated, use the same case as the object property instead.'); - $object = (new Instantiator())->alwaysForceProperties()([ + $object = Instantiator::withConstructor()->alwaysForceProperties()([ 'prop-a' => 'A', 'prop-b' => 'B', 'prop-c' => 'C', @@ -454,7 +559,7 @@ public function always_force_mode_throws_exception_for_extra_attributes(): void $this->expectException(\InvalidArgumentException::class); $this->expectExceptionMessage('Class "Zenstruck\Foundry\Tests\Unit\InstantiatorDummy" does not have property "extra".'); - (new Instantiator())->alwaysForceProperties()([ + Instantiator::withConstructor()->alwaysForce()([ 'propB' => 'B', 'extra' => 'foo', ], InstantiatorDummy::class); @@ -468,7 +573,7 @@ public function always_force_mode_allows_optional_attribute_name_prefix(): void { $this->expectDeprecation('Since zenstruck\foundry 1.5.0: Using "optional:" attribute prefixes is deprecated, use Instantiator::allowExtraAttributes() instead (https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#instantiation).'); - $object = (new Instantiator())->alwaysForceProperties()([ + $object = Instantiator::withConstructor()->alwaysForceProperties()([ 'propB' => 'B', 'propD' => 'D', 'optional:extra' => 'foo', @@ -482,7 +587,7 @@ public function always_force_mode_allows_optional_attribute_name_prefix(): void */ public function always_force_mode_with_allow_extra_attributes_mode(): void { - $object = (new Instantiator())->allowExtraAttributes()->alwaysForceProperties()([ + $object = Instantiator::withConstructor()->allowExtra()->alwaysForce()([ 'propB' => 'B', 'propD' => 'D', 'extra' => 'foo', @@ -496,7 +601,7 @@ public function always_force_mode_with_allow_extra_attributes_mode(): void */ public function always_force_mode_can_set_parent_class_properties(): void { - $object = (new Instantiator())->alwaysForceProperties()([ + $object = Instantiator::withConstructor()->alwaysForce()([ 'propA' => 'A', 'propB' => 'B', 'propC' => 'C', @@ -509,7 +614,6 @@ public function always_force_mode_can_set_parent_class_properties(): void $this->assertSame('constructor B', $object->getPropB()); $this->assertSame('constructor C', $object->getPropC()); $this->assertSame('D', $object->getPropD()); - $this->assertSame('E', Instantiator::forceGet($object, 'propE')); } /** @@ -519,7 +623,7 @@ public function invalid_attribute_type_with_allow_extra_enabled_throws_exception { $this->expectException(\Throwable::class); - (new Instantiator())->allowExtraAttributes()([ + Instantiator::withConstructor()->allowExtra()([ 'propB' => 'B', 'propF' => 'F', ], InstantiatorDummy::class); @@ -530,7 +634,7 @@ public function invalid_attribute_type_with_allow_extra_enabled_throws_exception */ public function can_set_variadic_constructor_attributes(): void { - $object = (new Instantiator())([ + $object = Instantiator::withConstructor()([ 'propA' => 'A', 'propB' => ['B', 'C', 'D'], ], VariadicInstantiatorDummy::class); @@ -542,11 +646,11 @@ public function can_set_variadic_constructor_attributes(): void /** * @test */ - public function missing_variadic_argument_thtrows(): void + public function missing_variadic_argument_throws(): void { $this->expectException(\InvalidArgumentException::class); $this->expectExceptionMessage('Missing constructor argument "propB" for "Zenstruck\Foundry\Tests\Unit\VariadicInstantiatorDummy".'); - (new Instantiator())([ + Instantiator::withConstructor()([ 'propA' => 'A', ], VariadicInstantiatorDummy::class); } diff --git a/tests/Unit/ModelFactoryTest.php b/tests/Unit/ModelFactoryTest.php index 65c540a86..37cef4a53 100644 --- a/tests/Unit/ModelFactoryTest.php +++ b/tests/Unit/ModelFactoryTest.php @@ -37,6 +37,16 @@ public function can_set_states_with_method(): void /** * @test + * @group legacy + */ + public function can_set_states_with_legacy_method(): void + { + $this->assertTrue(PostFactory::new()->publishedWithLegacyMethod()->create()->isPublished()); + } + + /** + * @test + * @group legacy */ public function can_set_state_via_new(): void { diff --git a/tests/Unit/ProxyTest.php b/tests/Unit/ProxyTest.php index f29acd87c..997530be7 100644 --- a/tests/Unit/ProxyTest.php +++ b/tests/Unit/ProxyTest.php @@ -15,7 +15,8 @@ use Doctrine\Persistence\ObjectManager; use PHPUnit\Framework\TestCase; use Zenstruck\Foundry\Factory; -use Zenstruck\Foundry\Proxy; +use Zenstruck\Foundry\Persistence\Proxy; +use Zenstruck\Foundry\Proxy as ProxyObject; use Zenstruck\Foundry\Test\Factories; use Zenstruck\Foundry\Tests\Fixtures\Entity\Category; @@ -31,21 +32,22 @@ final class ProxyTest extends TestCase */ public function can_force_get_and_set_non_public_properties(): void { - $proxy = new Proxy(new Category()); + $proxy = new ProxyObject(new Category()); - $this->assertNull($proxy->forceGet('name')); + $this->assertNull($proxy->_get('name')); - $proxy->forceSet('name', 'foo'); + $proxy->_set('name', 'foo'); - $this->assertSame('foo', $proxy->forceGet('name')); + $this->assertSame('foo', $proxy->_get('name')); } /** * @test + * @group legacy */ public function can_access_wrapped_objects_properties(): void { - $proxy = new Proxy(new class() { + $proxy = new ProxyObject(new class() { public $property; }); @@ -70,11 +72,12 @@ public function cannot_refresh_unpersisted_proxy(): void $this->expectException(\RuntimeException::class); $this->expectExceptionMessage('Cannot refresh unpersisted object (Zenstruck\Foundry\Tests\Fixtures\Entity\Category).'); - (new Proxy(new Category()))->refresh(); + (new ProxyObject(new Category()))->_refresh(); } /** * @test + * @group legacy */ public function saving_unpersisted_proxy_changes_it_to_a_persisted_proxy(): void { @@ -87,11 +90,11 @@ public function saving_unpersisted_proxy_changes_it_to_a_persisted_proxy(): void Factory::configuration()->setManagerRegistry($registry)->enableDefaultProxyAutoRefresh(); - $category = new Proxy(new Category()); + $category = new ProxyObject(new Category()); $this->assertFalse($category->isPersisted()); - $category->save(); + $category->_save(); $this->assertTrue($category->isPersisted()); } @@ -101,37 +104,48 @@ public function saving_unpersisted_proxy_changes_it_to_a_persisted_proxy(): void */ public function can_use_without_auto_refresh_with_proxy_or_object_typehint(): void { - $proxy = new Proxy(new Category()); + $proxy = new ProxyObject(new Category()); $calls = 0; $proxy - ->withoutAutoRefresh(static function(Proxy $proxy) use (&$calls): void { + ->_withoutAutoRefresh(static function(Proxy $proxy) use (&$calls): void { ++$calls; }) - ->withoutAutoRefresh(static function(Category $category) use (&$calls): void { + ->_withoutAutoRefresh(static function(Category $category) use (&$calls): void { ++$calls; }) - ->withoutAutoRefresh(function($proxy) use (&$calls): void { + ->_withoutAutoRefresh(function($proxy) use (&$calls): void { $this->assertInstanceOf(Proxy::class, $proxy); ++$calls; }) - ->withoutAutoRefresh(static function() use (&$calls): void { + ->_withoutAutoRefresh(static function() use (&$calls): void { ++$calls; }) - ->withoutAutoRefresh(fn(Proxy $proxy) => $this->functionWithProxy($proxy)) - ->withoutAutoRefresh(fn(Category $category) => $this->functionWithObject($category)) - ->withoutAutoRefresh(fn($proxy) => $this->functionWithNoTypeHint($proxy)) - ->withoutAutoRefresh(static fn(Proxy $proxy) => self::staticFunctionWithProxy($proxy)) - ->withoutAutoRefresh(static fn(Category $category) => self::staticFunctionWithObject($category)) - ->withoutAutoRefresh(static fn($proxy) => self::staticFunctionWithNoTypeHint($proxy)) + ->_withoutAutoRefresh(fn(Proxy $proxy) => $this->functionWithProxy($proxy)) + ->_withoutAutoRefresh(fn(Category $category) => $this->functionWithObject($category)) + ->_withoutAutoRefresh(fn($proxy) => $this->functionWithNoTypeHint($proxy)) + ->_withoutAutoRefresh(static fn(Proxy $proxy) => self::staticFunctionWithProxy($proxy)) + ->_withoutAutoRefresh(static fn(Category $category) => self::staticFunctionWithObject($category)) + ->_withoutAutoRefresh(static fn($proxy) => self::staticFunctionWithNoTypeHint($proxy)) ; $this->assertSame(4, $calls); } + /** + * @test + */ + public function can_use_new_class_as_legacy_one(): void + { + $proxy = new ProxyObject(new Category()); + + self::assertInstanceOf(ProxyObject::class, $proxy); + self::assertInstanceOf(Proxy::class, $proxy); + } + public function functionWithProxy(Proxy $proxy): void { - $this->assertInstanceOf(Category::class, $proxy->object()); + $this->assertInstanceOf(Category::class, $proxy->_real()); } public function functionWithObject(Category $category): void @@ -142,12 +156,12 @@ public function functionWithObject(Category $category): void public function functionWithNoTypeHint($proxy): void { $this->assertInstanceOf(Proxy::class, $proxy); - $this->assertInstanceOf(Category::class, $proxy->object()); + $this->assertInstanceOf(Category::class, $proxy->_real()); } public static function staticFunctionWithProxy(Proxy $proxy): void { - self::assertInstanceOf(Category::class, $proxy->object()); + self::assertInstanceOf(Category::class, $proxy->_real()); } public static function staticFunctionWithObject(Category $category): void @@ -158,6 +172,6 @@ public static function staticFunctionWithObject(Category $category): void public static function staticFunctionWithNoTypeHint($proxy): void { self::assertInstanceOf(Proxy::class, $proxy); - self::assertInstanceOf(Category::class, $proxy->object()); + self::assertInstanceOf(Category::class, $proxy->_real()); } } diff --git a/tests/Unit/RepositoryProxyTest.php b/tests/Unit/RepositoryDecoratorTest.php similarity index 77% rename from tests/Unit/RepositoryProxyTest.php rename to tests/Unit/RepositoryDecoratorTest.php index 4a00a9f2a..2ae4c7b9b 100644 --- a/tests/Unit/RepositoryProxyTest.php +++ b/tests/Unit/RepositoryDecoratorTest.php @@ -13,20 +13,21 @@ use Doctrine\Persistence\ObjectRepository; use PHPUnit\Framework\TestCase; -use Zenstruck\Foundry\RepositoryProxy; +use Zenstruck\Foundry\Persistence\RepositoryDecorator; /** * @author Kevin Bond */ -final class RepositoryProxyTest extends TestCase +final class RepositoryDecoratorTest extends TestCase { /** * @test + * @group legacy * @dataProvider objectRepositoryWithoutFindOneByOrderBy */ public function calling_find_one_by_with_order_by_when_wrapped_repo_does_not_have_throws_exception(ObjectRepository $inner): void { - $proxy = new RepositoryProxy($inner); + $proxy = new RepositoryDecorator($inner); $this->expectException(\RuntimeException::class); @@ -35,25 +36,25 @@ public function calling_find_one_by_with_order_by_when_wrapped_repo_does_not_hav public static function objectRepositoryWithoutFindOneByOrderBy(): iterable { - yield [new RepositoryProxy(new class() extends RepositoryStub { + yield [new RepositoryDecorator(new class() extends RepositoryStub { public function findOneBy(array $criteria): void { } })]; - yield [new RepositoryProxy(new class() extends RepositoryStub { + yield [new RepositoryDecorator(new class() extends RepositoryStub { public function findOneBy(array $criteria, ?array $foo = null): void { } })]; - yield [new RepositoryProxy(new class() extends RepositoryStub { + yield [new RepositoryDecorator(new class() extends RepositoryStub { public function findOneBy(array $criteria, $orderBy = null): void { } })]; - yield [new RepositoryProxy(new class() extends RepositoryStub { + yield [new RepositoryDecorator(new class() extends RepositoryStub { public function findOneBy(array $criteria, ?string $orderBy = null): void { } @@ -67,7 +68,7 @@ public function can_get_inner_repository(): void { $inner = $this->createMock(ObjectRepository::class); - $repository = new RepositoryProxy($inner); + $repository = new RepositoryDecorator($inner); $this->assertSame($inner, $repository->inner()); } diff --git a/utils/rector/composer.json b/utils/rector/composer.json new file mode 100644 index 000000000..57d347424 --- /dev/null +++ b/utils/rector/composer.json @@ -0,0 +1,32 @@ +{ + "name": "zenstruck/foundry-rector", + "autoload": { + "psr-4": { + "Zenstruck\\Foundry\\Utils\\Rector\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Zenstruck\\Foundry\\Utils\\Rector\\Tests\\": "tests/" + } + }, + "authors": [ + { + "name": "Nicolas PHILIPPE", + "email": "nikophil@gmail.com" + } + ], + "require-dev": { + "rector/rector": "^1.0", + "phpunit/phpunit": "^10.5", + "phpstan/phpstan-doctrine": "^1.3", + "zenstruck/foundry": "dev-1.x-bc", + "symfony/var-dumper": "^7.0", + "doctrine/orm": "^2.17", + "symfony/framework-bundle": "^7.0", + "doctrine/mongodb-odm": "^2.6" + }, + "require": { + "symfony/cache": "^7.0" + } +} diff --git a/utils/rector/config/foundry-set.php b/utils/rector/config/foundry-set.php new file mode 100644 index 000000000..bfbe0bcc3 --- /dev/null +++ b/utils/rector/config/foundry-set.php @@ -0,0 +1,70 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Rector\Config\RectorConfig; +use Rector\Renaming\Rector\MethodCall\RenameMethodRector; +use Rector\Renaming\ValueObject\MethodCallRename; +use Rector\Transform\Rector\MethodCall\MethodCallToPropertyFetchRector; +use Rector\Transform\ValueObject\MethodCallToPropertyFetch; +use Zenstruck\Foundry\FactoryCollection; +use Zenstruck\Foundry\Utils\Rector\AddProxyToFactoryCollectionTypeInPhpDoc; +use Zenstruck\Foundry\Utils\Rector\ChangeDisableEnablePersist; +use Zenstruck\Foundry\Utils\Rector\ChangeFactoryBaseClass; +use Zenstruck\Foundry\Utils\Rector\ChangeFactoryMethodCalls; +use Zenstruck\Foundry\Utils\Rector\ChangeFunctionsCalls; +use Zenstruck\Foundry\Utils\Rector\ChangeInstantiatorMethodCalls; +use Zenstruck\Foundry\Utils\Rector\ChangeLegacyClassImports; +use Zenstruck\Foundry\Utils\Rector\ChangeProxyMethodCalls; +use Zenstruck\Foundry\Utils\Rector\ChangeStaticFactoryFakerCalls; +use Zenstruck\Foundry\Utils\Rector\PersistenceResolver; +use Zenstruck\Foundry\Utils\Rector\RemoveProxyRealObjectMethodCallsForNotProxifiedObjects; +use Zenstruck\Foundry\Utils\Rector\RemoveUnproxifyArrayMap; +use Zenstruck\Foundry\Utils\Rector\RuleRequirementsChecker; + +return static function(RectorConfig $rectorConfig): void { + RuleRequirementsChecker::checkRequirements(); + + /** + * This must be overridden in user's `rector.php` to provide a path to the object manager. + * @see https://github.com/phpstan/phpstan-doctrine#configuration + */ + $rectorConfig->singleton(PersistenceResolver::class); + + $rectorConfig->rules([ + ChangeFactoryBaseClass::class, + ChangeLegacyClassImports::class, + RemoveProxyRealObjectMethodCallsForNotProxifiedObjects::class, + ChangeInstantiatorMethodCalls::class, + ChangeDisableEnablePersist::class, + AddProxyToFactoryCollectionTypeInPhpDoc::class, + ChangeFactoryMethodCalls::class, + ChangeFunctionsCalls::class, + ChangeProxyMethodCalls::class, + ChangeStaticFactoryFakerCalls::class, + RemoveUnproxifyArrayMap::class, + ]); + + $rectorConfig->ruleWithConfiguration( + RenameMethodRector::class, + [ + new MethodCallRename(FactoryCollection::class, 'set', 'many'), + ] + ); + + $rectorConfig->ruleWithConfiguration( + MethodCallToPropertyFetchRector::class, + [ + new MethodCallToPropertyFetch(FactoryCollection::class, 'factory', 'factory'), + ] + ); +}; diff --git a/utils/rector/phpstan.neon b/utils/rector/phpstan.neon new file mode 100644 index 000000000..0174e233b --- /dev/null +++ b/utils/rector/phpstan.neon @@ -0,0 +1,14 @@ +parameters: + inferPrivatePropertyTypeFromConstructor: true + checkMissingIterableValueType: false + checkUninitializedProperties: true + checkGenericClassInNonGenericObjectType: false + paths: + - ./src + - ./tests + level: 8 + bootstrapFiles: + - ./vendor/autoload.php + excludePaths: + - ./tests/Fixtures + diff --git a/utils/rector/phpunit.xml.dist b/utils/rector/phpunit.xml.dist new file mode 100644 index 000000000..603dda9ab --- /dev/null +++ b/utils/rector/phpunit.xml.dist @@ -0,0 +1,14 @@ + + + + + + ./tests/ + + + diff --git a/utils/rector/src/AddProxyToFactoryCollectionTypeInPhpDoc.php b/utils/rector/src/AddProxyToFactoryCollectionTypeInPhpDoc.php new file mode 100644 index 000000000..c277f320f --- /dev/null +++ b/utils/rector/src/AddProxyToFactoryCollectionTypeInPhpDoc.php @@ -0,0 +1,163 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Utils\Rector; + +use PhpParser\Node; +use PHPStan\PhpDocParser\Ast\PhpDoc\ParamTagValueNode; +use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagNode; +use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode; +use PHPStan\PhpDocParser\Ast\Type\TypeNode; +use Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfoFactory; +use Rector\BetterPhpDocParser\ValueObject\Type\FullyQualifiedIdentifierTypeNode; +use Rector\Comments\NodeDocBlock\DocBlockUpdater; +use Rector\Rector\AbstractRector; +use Rector\StaticTypeMapper\StaticTypeMapper; +use Rector\StaticTypeMapper\ValueObject\Type\AliasedObjectType; +use Rector\StaticTypeMapper\ValueObject\Type\FullyQualifiedObjectType; +use Rector\StaticTypeMapper\ValueObject\Type\ShortenedObjectType; +use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample; +use Symplify\RuleDocGenerator\ValueObject\RuleDefinition; +use Zenstruck\Foundry\FactoryCollection; +use Zenstruck\Foundry\Persistence\Proxy; + +final class AddProxyToFactoryCollectionTypeInPhpDoc extends AbstractRector +{ + private PersistenceResolver $persistenceResolver; + + public function __construct( + private PhpDocInfoFactory $phpDocInfoFactory, + private StaticTypeMapper $staticTypeMapper, + private DocBlockUpdater $docBlockUpdater, + ?PersistenceResolver $persistenceResolver, + ) { + $this->persistenceResolver = $persistenceResolver ?? new PersistenceResolver(); + } + + public function getRuleDefinition(): RuleDefinition + { + return new RuleDefinition( + 'Add "Proxy" generic type to FactoryCollection in docblock if missing. This will only affect persistent objects using proxy.', + [ + new CodeSample( + <<<'CODE_SAMPLE' + /** + * @param FactoryCollection $factoryCollection + */ + public function foo(FactoryCollection $factoryCollection); + CODE_SAMPLE, + <<<'CODE_SAMPLE' + /** + * @param FactoryCollection<\Zenstruck\Foundry\Persistence\Proxy> $factoryCollection + */ + public function foo(FactoryCollection $factoryCollection); + CODE_SAMPLE + ), + ] + ); + } + + /** + * @return array> + */ + public function getNodeTypes(): array + { + return [Node\Stmt\ClassMethod::class]; + } + + public function refactor(Node $node): ?Node + { + return match ($node::class) { + Node\Stmt\ClassMethod::class => $this->changeParamFactoryCollection($node) ? $node : null, + default => null, + }; + } + + private function changeParamFactoryCollection(Node\Stmt\ClassMethod $node): bool + { + $phpDocInfo = $this->phpDocInfoFactory->createFromNode($node); + + if (!$phpDocInfo) { + return false; + } + + $hasChanged = false; + + $phpDocNode = $phpDocInfo->getPhpDocNode(); + foreach ($phpDocNode->children as $phpDocChildNode) { + // assert we have a param with format `@param FactoryCollection` + if (!$phpDocChildNode instanceof PhpDocTagNode + || '@param' !== $phpDocChildNode->name + || !$phpDocChildNode->value instanceof ParamTagValueNode + || !($genericTypeNode = $phpDocChildNode->value->type) instanceof GenericTypeNode + || !\str_contains((string) $phpDocChildNode, 'FactoryCollection<') + || 1 !== \count($genericTypeNode->genericTypes) + ) { + continue; + } + + // assert we really have a `FactoryCollection` from foundry + if (FactoryCollection::class !== $this->getFullyQualifiedClassName($genericTypeNode->type, $node)) { + continue; + } + + // handle case FactoryCollection> + if (($factoryGenericType = $genericTypeNode->genericTypes[0]) instanceof GenericTypeNode) { + if (1 === \count($factoryGenericType->genericTypes)) { + $proxy = $this->getFullyQualifiedClassName($factoryGenericType->type, $node); + $proxyTargetClassName = $this->getFullyQualifiedClassName($factoryGenericType->genericTypes[0], $node); + + if (!$proxy || !$proxyTargetClassName) { + continue; + } + + if (\is_a($proxy, Proxy::class, allow_string: true) && !$this->persistenceResolver->shouldUseProxyFactory($proxyTargetClassName)) { + $hasChanged = true; + $phpDocChildNode->value->type->genericTypes = [new FullyQualifiedIdentifierTypeNode($proxyTargetClassName)]; + } + } + + continue; + } + + // assert generic type will effectively come from PersistentProxyObjectFactory + $targetClassName = $this->getFullyQualifiedClassName($genericTypeNode->genericTypes[0], $node); + if (!$targetClassName || !$this->persistenceResolver->shouldUseProxyFactory($targetClassName)) { + continue; + } + + $hasChanged = true; + $phpDocChildNode->value->type->genericTypes = [new GenericTypeNode(new FullyQualifiedIdentifierTypeNode(Proxy::class), $genericTypeNode->genericTypes)]; + } + + if ($hasChanged) { + $this->docBlockUpdater->updateRefactoredNodeWithPhpDocInfo($node); + } + + return $hasChanged; + } + + /** + * @return class-string|null + */ + private function getFullyQualifiedClassName(TypeNode $typeNode, Node $node): ?string + { + $type = $this->staticTypeMapper->mapPHPStanPhpDocTypeNodeToPHPStanType($typeNode, $node); + + return match ($type::class) { // @phpstan-ignore-line + FullyQualifiedObjectType::class => $type->getClassName(), + ShortenedObjectType::class, AliasedObjectType::class => $type->getFullyQualifiedName(), + default => null, + }; + } +} diff --git a/utils/rector/src/ChangeDisableEnablePersist.php b/utils/rector/src/ChangeDisableEnablePersist.php new file mode 100644 index 000000000..93ad86d0c --- /dev/null +++ b/utils/rector/src/ChangeDisableEnablePersist.php @@ -0,0 +1,145 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Utils\Rector; + +use PhpParser\Node; +use PhpParser\NodeTraverser; +use PHPStan\Type\ThisType; +use PHPUnit\Framework\TestCase; +use Rector\Rector\AbstractRector; +use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; +use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample; +use Symplify\RuleDocGenerator\ValueObject\RuleDefinition; + +final class ChangeDisableEnablePersist extends AbstractRector +{ + public function getRuleDefinition(): RuleDefinition + { + return new RuleDefinition( + 'Deprecated functions Factories::disablePersist() should either be moved to new persistence function if called in a KernelTestCase or removed otherwise.', + [ + new CodeSample( + <<<'CODE_SAMPLE' + class SomeTest extends TestCase + { + use Factories; + + protected function setUp(): void + { + $this->enablePersist(); + $this->disablePersist(); + } + } + CODE_SAMPLE, + <<enablePersist(); + $this->disablePersist(); + } + } + CODE_SAMPLE, + <<> + */ + public function getNodeTypes(): array + { + return [Node\Stmt\Expression::class]; + } + + /** + * @param Node\Stmt\Expression $node + */ + public function refactor(Node $node): Node|int|null + { + $expr = $node->expr; + + if (!$expr instanceof Node\Expr\MethodCall) { + return null; + } + + if ($this->isCallOnThisInPhpUnitTestCase($expr)) { + return match ($this->getName($expr->name)) { + 'disablePersist', 'enablePersist' => NodeTraverser::REMOVE_NODE, + default => null, + }; + } elseif ($this->isCallOnThisInPhpUnitKernelTestCase($expr)) { + return match ($this->getName($expr->name)) { + 'disablePersist' => new Node\Stmt\Expression( + new Node\Expr\FuncCall( + new Node\Name('\Zenstruck\Foundry\Persistence\disable_persisting'), $expr->args + ) + ), + 'enablePersist' => new Node\Stmt\Expression( + new Node\Expr\FuncCall( + new Node\Name('\Zenstruck\Foundry\Persistence\enable_persisting'), $expr->args + ) + ), + default => null, + }; + } + + return null; + } + + private function isCallOnThisInPhpUnitTestCase(Node\Expr\MethodCall $node): bool + { + $type = $this->getType($node->var); + + return $type instanceof ThisType + && \is_a($type->getClassName(), TestCase::class, allow_string: true) + && !\is_a($type->getClassName(), KernelTestCase::class, allow_string: true); + } + + private function isCallOnThisInPhpUnitKernelTestCase(Node\Expr\MethodCall $node): bool + { + $type = $this->getType($node->var); + + return $type instanceof ThisType + && \is_a($type->getClassName(), KernelTestCase::class, allow_string: true); + } +} diff --git a/utils/rector/src/ChangeFactoryBaseClass.php b/utils/rector/src/ChangeFactoryBaseClass.php new file mode 100644 index 000000000..eacec29e0 --- /dev/null +++ b/utils/rector/src/ChangeFactoryBaseClass.php @@ -0,0 +1,346 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Utils\Rector; + +use PhpParser\Node; +use PhpParser\Node\Stmt\Class_; +use PHPStan\Analyser\MutatingScope; +use PHPStan\PhpDocParser\Ast\PhpDoc\MethodTagValueNode; +use PHPStan\PhpDocParser\Ast\PhpDoc\MethodTagValueParameterNode; +use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode; +use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; +use PHPStan\PhpDocParser\Ast\Type\UnionTypeNode; +use PHPStan\Type\ObjectType; +use Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfoFactory; +use Rector\BetterPhpDocParser\PhpDocManipulator\PhpDocTagRemover; +use Rector\BetterPhpDocParser\ValueObject\Type\BracketsAwareIntersectionTypeNode; +use Rector\BetterPhpDocParser\ValueObject\Type\BracketsAwareUnionTypeNode; +use Rector\BetterPhpDocParser\ValueObject\Type\FullyQualifiedIdentifierTypeNode; +use Rector\BetterPhpDocParser\ValueObject\Type\SpacingAwareArrayTypeNode; +use Rector\Comments\NodeDocBlock\DocBlockUpdater; +use Rector\Rector\AbstractRector; +use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample; +use Symplify\RuleDocGenerator\ValueObject\RuleDefinition; +use Zenstruck\Foundry\ModelFactory; +use Zenstruck\Foundry\ObjectFactory; +use Zenstruck\Foundry\Persistence\PersistentProxyObjectFactory; +use Zenstruck\Foundry\Persistence\ProxyRepositoryDecorator; + +final class ChangeFactoryBaseClass extends AbstractRector +{ + public function __construct( + private PhpDocTagRemover $phpDocTagRemover, + private PhpDocInfoFactory $phpDocInfoFactory, + private DocBlockUpdater $docBlockUpdater, + private PersistenceResolver $persistenceResolver, + ) { + } + + public function getRuleDefinition(): RuleDefinition + { + return new RuleDefinition( + << + * + * @method DummyObject|Proxy create(array|callable $attributes = []) + * @method static DummyObject|Proxy createOne(array $attributes = []) + * @method static DummyObject[]|Proxy[] createMany(int $number, array|callable $attributes = []) + * @method static DummyObject[]|Proxy[] createSequence(iterable|callable $sequence) + */ + final class DummyObjectFactory extends ModelFactory + { + protected function getDefaults(): array + { + return []; + } + + protected function initialize(): self + { + return $this; + } + + protected static function getClass(): string + { + return DummyObject::class; + } + } + CODE_SAMPLE, + <<<'CODE_SAMPLE' + /** + * @extends \Zenstruck\Foundry\ObjectFactory + */ + final class DummyObjectFactory extends \Zenstruck\Foundry\ObjectFactory + { + protected function defaults(): array + { + return []; + } + + protected function initialize(): static + { + return $this; + } + + public static function class(): string + { + return DummyObject::class; + } + } + CODE_SAMPLE + ), + new CodeSample( + <<<'CODE_SAMPLE' + /** + * @extends ModelFactory + * + * @method static RepositoryProxy|EntityRepository repository() + * @method static RepositoryProxy repository() + * @method DummyPersistentObject|Proxy create(array|callable $attributes = []) + * + * @phpstan-method Proxy create(array|callable $attributes = []) + * @phpstan-method static Proxy createOne(array $attributes = []) + * @phpstan-method static RepositoryProxy repository() + */ + final class DummyPersistentProxyFactory extends ModelFactory + { + protected function getDefaults(): array + { + return []; + } + + protected function initialize(): self + { + return $this; + } + + protected static function getClass(): string + { + return DummyPersistentObject::class; + } + } + CODE_SAMPLE, + <<<'CODE_SAMPLE' + /** + * @extends \Zenstruck\Foundry\Persistence\PersistentProxyObjectFactory + * + * @method static EntityRepository|\Zenstruck\Foundry\Persistence\RepositoryDecorator repository() + * @method static \Zenstruck\Foundry\Persistence\RepositoryDecorator repository() + * @method DummyPersistentObject|Proxy create(array|callable $attributes = []) + * + * @phpstan-method Proxy create(array|callable $attributes = []) + * @phpstan-method static Proxy createOne(array $attributes = []) + * @phpstan-method static \Zenstruck\Foundry\Persistence\RepositoryDecorator repository() + */ + final class DummyPersistentProxyFactory extends \Zenstruck\Foundry\Persistence\PersistentProxyObjectFactory + { + protected function defaults(): array + { + return []; + } + + protected function initialize(): static + { + return $this; + } + + public static function class(): string + { + return DummyPersistentObject::class; + } + } + CODE_SAMPLE + ), + ] + ); + } + + /** + * @return array> + */ + public function getNodeTypes(): array + { + return [Class_::class]; + } + + /** + * @param Class_ $node + */ + public function refactor(Node $node): ?Node + { + if (!$this->isObjectType($node, new ObjectType(ModelFactory::class))) { + return null; + } + + $this->changeBaseClass($node); + $this->changeFactoryMethods($node); + + return $node; + } + + private function changeBaseClass(Class_ $node): ?Class_ + { + /** @var MutatingScope $mutatingScope */ + $mutatingScope = $node->getAttribute('scope'); + $parentFactoryReflection = $mutatingScope->getClassReflection()?->getParentClass(); + + if (!$parentFactoryReflection) { + return null; + } + + if ( + !\str_starts_with($parentFactoryReflection->getName(), 'Zenstruck\Foundry') + || \str_starts_with($parentFactoryReflection->getName(), 'Zenstruck\Foundry\Utils\Rector') + ) { + $newFactoryClass = $parentFactoryReflection->getName(); + } elseif ($this->persistenceResolver->shouldTransformFactoryIntoObjectFactory($this->getName($node))) { // @phpstan-ignore-line + $newFactoryClass = ObjectFactory::class; + $node->extends = new Node\Name\FullyQualified($newFactoryClass); + } else { + $newFactoryClass = PersistentProxyObjectFactory::class; + $node->extends = new Node\Name\FullyQualified($newFactoryClass); + } + + $this->updatePhpDoc($node, $newFactoryClass); + $this->docBlockUpdater->updateRefactoredNodeWithPhpDocInfo($node); + + return $node; + } + + /** + * - updates `@extends` annotation + * - removes `@method` annotations when not using proxies anymore + * - change `@[phpstan|psalm]-method` annotations and rewrite them. + */ + private function updatePhpDoc(Class_ $node, string $newFactoryClass): void + { + $phpDocInfo = $this->phpDocInfoFactory->createFromNode($node); + + if (!$phpDocInfo) { + return; + } + + $phpDocNode = $phpDocInfo->getPhpDocNode(); + + $extendsDocNode = $phpDocNode->getExtendsTagValues()[0] ?? null; + if ($extendsDocNode) { + $extendsDocNode->type->type = new FullyQualifiedIdentifierTypeNode($newFactoryClass); + } + + if ($newFactoryClass === ObjectFactory::class) { + $this->phpDocTagRemover->removeByName($phpDocInfo, 'method'); + $this->phpDocTagRemover->removeByName($phpDocInfo, 'phpstan-method'); + $this->phpDocTagRemover->removeByName($phpDocInfo, 'psalm-method'); + } else { + $targetClassName = $this->persistenceResolver->targetClass($this->getName($node->namespacedName)); // @phpstan-ignore-line + + /** @var MethodTagValueNode[] $methodNodes */ + $methodNodes = [ + ...$phpDocNode->getMethodTagValues(), + ...$phpDocNode->getMethodTagValues('@phpstan-method'), + ...$phpDocNode->getMethodTagValues('@psalm-method'), + ]; + + foreach ($methodNodes as $methodNode) { + if (\in_array($methodNode->methodName, ['create', 'createOne', 'find', 'findOrCreate', 'first', 'last', 'random', 'randomOrCreate'], true)) { + if (\str_contains($methodNode->getAttribute('parent')->name, '-method')) { + $methodNode->returnType = new BracketsAwareIntersectionTypeNode( + [ + new FullyQualifiedIdentifierTypeNode($targetClassName), + new GenericTypeNode(new IdentifierTypeNode('Proxy'), [new FullyQualifiedIdentifierTypeNode($targetClassName)]), + ] + ); + } else { + $methodNode->returnType = new BracketsAwareUnionTypeNode( + [ + new FullyQualifiedIdentifierTypeNode($targetClassName), + new IdentifierTypeNode('Proxy'), + ] + ); + } + } elseif (\in_array($methodNode->methodName, ['all', 'createMany', 'createSequence', 'findBy', 'randomRange', 'randomSet'], true)) { + if (\str_contains($methodNode->getAttribute('parent')->name, '-method')) { + $methodNode->returnType = new GenericTypeNode( + new IdentifierTypeNode('list'), + [ + new BracketsAwareIntersectionTypeNode( + [ + new FullyQualifiedIdentifierTypeNode($targetClassName), + new GenericTypeNode(new IdentifierTypeNode('Proxy'), [new FullyQualifiedIdentifierTypeNode($targetClassName)]), + ] + ), + ] + ); + } else { + $methodNode->returnType = new BracketsAwareUnionTypeNode( + [ + new SpacingAwareArrayTypeNode(new FullyQualifiedIdentifierTypeNode($targetClassName)), + new SpacingAwareArrayTypeNode(new IdentifierTypeNode('Proxy')), + ] + ); + } + } elseif ('repository' === $methodNode->methodName) { + $methodNode->returnType = new GenericTypeNode( + new FullyQualifiedIdentifierTypeNode(ProxyRepositoryDecorator::class), + [ + new FullyQualifiedIdentifierTypeNode($targetClassName), + new FullyQualifiedIdentifierTypeNode($this->persistenceResolver->geRepositoryClass($targetClassName)), + ] + ); + } + + // handle case when @method parameter is UnionType + // this prevents to render it with brackets, which creates parsing error + foreach ($methodNode->parameters as $parameter) { + if ($parameter instanceof MethodTagValueParameterNode && $parameter->type instanceof UnionTypeNode) { + $parameter->type = new BracketsAwareUnionTypeNode($parameter->type->types); + } + } + } + } + } + + private function changeFactoryMethods(Class_ $node): void + { + foreach ($node->getMethods() as $method) { + $methodName = $this->getName($method); + + if ('getDefaults' == $methodName) { + $method->name = new Node\Identifier('defaults'); + } + + if ('getClass' == $methodName) { + $method->name = new Node\Identifier('class'); + $method->flags = Class_::MODIFIER_PUBLIC | Class_::MODIFIER_STATIC; + } + + if ('initialize' == $methodName) { + $method->returnType = new Node\Identifier('static'); + } + } + } +} diff --git a/utils/rector/src/ChangeFactoryMethodCalls.php b/utils/rector/src/ChangeFactoryMethodCalls.php new file mode 100644 index 000000000..801049723 --- /dev/null +++ b/utils/rector/src/ChangeFactoryMethodCalls.php @@ -0,0 +1,135 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Utils\Rector; + +use PhpParser\Node; +use PhpParser\Node\Expr\MethodCall; +use PhpParser\Node\Expr\StaticCall; +use PHPStan\Type\ObjectType; +use Rector\Rector\AbstractRector; +use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample; +use Symplify\RuleDocGenerator\ValueObject\RuleDefinition; +use Zenstruck\Foundry\Factory; + +final class ChangeFactoryMethodCalls extends AbstractRector +{ + public function __construct( + private PersistenceResolver $persistenceResolver, + ) { + } + + /** + * @return array> + */ + public function getNodeTypes(): array + { + return [MethodCall::class, StaticCall::class]; + } + + public function getRuleDefinition(): RuleDefinition + { + return new RuleDefinition( + 'Change old factory methods to the new ones.', + [ + new CodeSample( + <<<'CODE_SAMPLE' + protected function someMethod() + { + DummyObjectFactory::new()->withAttributes(['publish' => true]); + } + CODE_SAMPLE, + <<<'CODE_SAMPLE' + protected function someMethod() + { + DummyObjectFactory::new()->with(['publish' => true]); + } + CODE_SAMPLE + ), + new CodeSample( + <<<'CODE_SAMPLE' + final class SomeFactory extends ObjectFactory + { + // mandatory functions... + + public function published(): static + { + return $this->addState(['publish' => true]); + } + } + CODE_SAMPLE, + <<<'CODE_SAMPLE' + final class SomeFactory extends ObjectFactory + { + // mandatory functions... + + public function published(): static + { + return $this->with(['publish' => true]); + } + } + CODE_SAMPLE + ), + ] + ); + } + + public function refactor(Node $node): Node|int|null + { + return match ($node::class) { + MethodCall::class => $this->changeMethodCall($node), + StaticCall::class => $this->changeStaticCall($node), + default => null, + }; + } + + public function changeMethodCall(MethodCall $node): Node|int|null + { + if (!$this->isObjectType($node->var, new ObjectType(Factory::class))) { + return null; + } + + if (\in_array($this->getName($node->name), ['addState', 'withAttributes'], true)) { + $node->name = new Node\Identifier('with'); + + return $node; + } + + if ('withoutPersisting' === $this->getName($node->name)) { + $type = $this->getType($node->var); + $classes = $type->getObjectClassNames(); + if (1 === \count($classes) && $this->persistenceResolver->shouldTransformFactoryIntoObjectFactory($classes[0])) { // @phpstan-ignore-line + return $node->var; + } + + return null; + } + + return null; + } + + private function changeStaticCall(StaticCall $node): ?Node + { + if (!$this->isObjectType($node->class, new ObjectType(Factory::class))) { + return null; + } + + if ('getDefaults' === $this->getName($node->name)) { + $node->name = new Node\Identifier('defaults'); + + return $node; + } + + return null; + } +} diff --git a/utils/rector/src/ChangeFunctionsCalls.php b/utils/rector/src/ChangeFunctionsCalls.php new file mode 100644 index 000000000..a1a750e37 --- /dev/null +++ b/utils/rector/src/ChangeFunctionsCalls.php @@ -0,0 +1,122 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Utils\Rector; + +use PhpParser\Node; +use PhpParser\Node\Identifier; +use Rector\Rector\AbstractRector; +use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample; +use Symplify\RuleDocGenerator\ValueObject\RuleDefinition; +use Zenstruck\Foundry\Factory; +use Zenstruck\Foundry\Test\TestState; +use Zenstruck\Foundry\Test\UnitTestConfig; + +final class ChangeFunctionsCalls extends AbstractRector +{ + /** + * @return array> + */ + public function getNodeTypes(): array + { + return [Node\Expr\FuncCall::class, Node\Expr\StaticCall::class]; + } + + public function getRuleDefinition(): RuleDefinition + { + return new RuleDefinition( + 'Change method calls of legacy functions and static calls.', + [ + new CodeSample( + << true]); + repository(\$someObject); + Factory::delayFlush(static fn() => true); + TestState::configure(faker: null); + CODE_SAMPLE, + << true])); + \\Zenstruck\\Foundry\\Persistence\repository(\$someObject); + \\Zenstruck\\Foundry\\Persistence\flush_after(static fn() => true); + \\Zenstruck\\Foundry\\Test\\UnitTestConfig::configure(faker: null); + CODE_SAMPLE + ), + ] + ); + } + + public function refactor(Node $node): Node|int|null + { + return match ($node::class) { + Node\Expr\FuncCall::class => $this->replaceFunctions($node), + Node\Expr\StaticCall::class => $this->replaceLegacyMethodCalls($node), + default => null, + }; + } + + private function replaceFunctions(Node\Expr\FuncCall $node): ?Node + { + if (!$node->name instanceof Node\Name) { + return null; + } + + $name = $node->name->getAttribute('namespacedName') ?? $this->getName($node->name); + + switch ($name) { + case 'Zenstruck\Foundry\create': + $node->name = new Node\Name\FullyQualified('Zenstruck\Foundry\Persistence\persist_proxy'); + + return $node; + case 'Zenstruck\Foundry\instantiate': + $node->name = new Node\Name\FullyQualified('Zenstruck\Foundry\object'); + + return new Node\Expr\FuncCall(new Node\Name\FullyQualified('Zenstruck\Foundry\Persistence\proxy'), [new Node\Arg($node)]); + case 'Zenstruck\Foundry\repository': + $node->name = new Node\Name\FullyQualified('Zenstruck\Foundry\Persistence\repository'); + + return $node; + default: + return null; + } + } + + private function replaceLegacyMethodCalls(Node\Expr\StaticCall $node): ?Node + { + if ( + $node->name instanceof Identifier + && 'delayFlush' === $this->getName($node->name) + && $node->class instanceof Node\Name + && \is_a((string) $node->class, Factory::class, allow_string: true) + ) { + return new Node\Expr\FuncCall(new Node\Name\FullyQualified('Zenstruck\Foundry\Persistence\flush_after'), $node->args); + } + + if ( + $node->name instanceof Identifier + && 'configure' === $this->getName($node->name) + && $node->class instanceof Node\Name + && TestState::class === (string) $node->class + ) { + return new Node\Expr\StaticCall(new Node\Name\FullyQualified(UnitTestConfig::class), 'configure', $node->args); + } + + return null; + } +} diff --git a/utils/rector/src/ChangeInstantiatorMethodCalls.php b/utils/rector/src/ChangeInstantiatorMethodCalls.php new file mode 100644 index 000000000..bb20e819e --- /dev/null +++ b/utils/rector/src/ChangeInstantiatorMethodCalls.php @@ -0,0 +1,128 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Utils\Rector; + +use PhpParser\Node; +use PhpParser\Node\Name\FullyQualified; +use PHPStan\Analyser\Scope; +use PHPStan\Type\ObjectType; +use Rector\Rector\AbstractScopeAwareRector; +use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample; +use Symplify\RuleDocGenerator\ValueObject\RuleDefinition; +use Zenstruck\Foundry\ModelFactory; +use Zenstruck\Foundry\Object\Instantiator; +use Zenstruck\Foundry\ObjectFactory; + +final class ChangeInstantiatorMethodCalls extends AbstractScopeAwareRector +{ + public function getRuleDefinition(): RuleDefinition + { + return new RuleDefinition( + 'Create Instantiator with named constructor + change legacy methods.', + [ + new CodeSample( + <<<'CODE_SAMPLE' + (new Instantiator()) + ->allowExtraAttributes(['some', 'fields']) + ->alwaysForceProperties(['other', 'fields']) + ->allowExtraAttributes() + ->alwaysForceProperties() + CODE_SAMPLE, + <<<'CODE_SAMPLE' + (\Zenstruck\Foundry\Object\Instantiator::withConstructor()) + ->allowExtra(...['some', 'fields']) + ->alwaysForce(...['other', 'fields']) + ->allowExtra() + ->alwaysForce() + CODE_SAMPLE + ), + ] + ); + } + + /** + * @return array> + */ + public function getNodeTypes(): array + { + return [Node\Expr\MethodCall::class, Node\Expr\New_::class]; + } + + public function refactorWithScope(Node $node, Scope $scope) + { + return match (true) { + $node instanceof Node\Expr\MethodCall => $this->changeMethodCalls($node), + $node instanceof Node\Expr\New_ => $this->useNamedConstructor($node, $scope), + default => null, + }; + } + + /** + * We cannot remove `->withoutConstructor()` calls without risk, so users should rely on deprecations. + */ + private function changeMethodCalls(Node\Expr\MethodCall $node): ?Node + { + if (!$this->isObjectType($node->var, new ObjectType(Instantiator::class))) { + return null; + } + + switch ($this->getName($node->name)) { + case 'allowExtraAttributes': + $node->name = new Node\Identifier('allowExtra'); + if (1 === \count($node->getArgs())) { + $node->getArgs()[0]->unpack = true; + } + + return $node; + case 'alwaysForceProperties': + $node->name = new Node\Identifier('alwaysForce'); + if (1 === \count($node->getArgs())) { + $node->getArgs()[0]->unpack = true; + } + + return $node; + default: return null; + } + } + + private function useNamedConstructor(Node\Expr\New_ $node, Scope $scope): ?Node + { + if (!$node->class instanceof FullyQualified) { + return null; + } + + if (!\is_a($node->class->toString(), Instantiator::class, allow_string: true)) { + return null; + } + + $factoryClass = $scope->getClassReflection()?->getName(); + if ($factoryClass && \is_a($factoryClass, ObjectFactory::class, allow_string: true)) { + $targetClass = \is_a($factoryClass, ModelFactory::class, allow_string: true) + ? (new \ReflectionClass($factoryClass))->getMethod('getClass')->invoke(null) + : $factoryClass::class(); + $targetClassConstructorIsPublic = (new \ReflectionClass($targetClass))->getConstructor()?->isPublic() ?? true; + + if (!$targetClassConstructorIsPublic) { + /** + * The only case where we can safely use `withoutConstructor()` is when target's class' constructor + * is not public: foundry 1.x fallbacks on "withoutConstructor" behavior, + * while foundry 2 throws an exception. + */ + return new Node\Expr\StaticCall(new Node\Name\FullyQualified(Instantiator::class), 'withoutConstructor'); + } + } + + return new Node\Expr\StaticCall(new Node\Name\FullyQualified(Instantiator::class), 'withConstructor'); + } +} diff --git a/utils/rector/src/ChangeLegacyClassImports.php b/utils/rector/src/ChangeLegacyClassImports.php new file mode 100644 index 000000000..961db2cb1 --- /dev/null +++ b/utils/rector/src/ChangeLegacyClassImports.php @@ -0,0 +1,89 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Utils\Rector; + +use PhpParser\Node; +use Rector\Rector\AbstractRector; +use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample; +use Symplify\RuleDocGenerator\ValueObject\RuleDefinition; +use Zenstruck\Foundry\Object\Instantiator; +use Zenstruck\Foundry\Persistence\Proxy; +use Zenstruck\Foundry\Persistence\RepositoryAssertions; +use Zenstruck\Foundry\Persistence\RepositoryDecorator; + +/** + * Let's only change type hints in "use" statements. + * + * We don't check for FQCN which would be used directly in PHPdoc, return types and parameter types, + * that would increase the complexity by a lot, for something people are not very likely to use. + */ +final class ChangeLegacyClassImports extends AbstractRector +{ + public function getRuleDefinition(): RuleDefinition + { + return new RuleDefinition( + 'Change FQCN in imports for some deprecated classes with their replacements.', + [ + new CodeSample( + <<<'CODE_SAMPLE' + use Zenstruck\Foundry\Proxy; + use Zenstruck\Foundry\Instantiator; + use Zenstruck\Foundry\RepositoryProxy; + use Zenstruck\Foundry\RepositoryAssertions; + CODE_SAMPLE, + <<<'CODE_SAMPLE' + use Zenstruck\Foundry\Persistence\Proxy; + use Zenstruck\Foundry\Object\Instantiator; + use Zenstruck\Foundry\Persistence\RepositoryDecorator; + use Zenstruck\Foundry\Persistence\RepositoryAssertions; + CODE_SAMPLE + ), + ] + ); + } + + /** + * @return array> + */ + public function getNodeTypes(): array + { + return [Node\Stmt\UseUse::class]; + } + + /** + * @param Node\Stmt\UseUse $node + */ + public function refactor(Node $node): ?Node + { + switch ($this->getName($node->name)) { + case \Zenstruck\Foundry\Proxy::class: + $node->name = new Node\Name(Proxy::class); + + return $node; + case \Zenstruck\Foundry\Instantiator::class: + $node->name = new Node\Name(Instantiator::class); + + return $node; + case \Zenstruck\Foundry\RepositoryProxy::class: + $node->name = new Node\Name(RepositoryDecorator::class); + + return $node; + case \Zenstruck\Foundry\RepositoryAssertions::class: + $node->name = new Node\Name(RepositoryAssertions::class); + + return $node; + default: return null; + } + } +} diff --git a/utils/rector/src/ChangeProxyMethodCalls.php b/utils/rector/src/ChangeProxyMethodCalls.php new file mode 100644 index 000000000..4ed2164af --- /dev/null +++ b/utils/rector/src/ChangeProxyMethodCalls.php @@ -0,0 +1,122 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Utils\Rector; + +use PhpParser\Node; +use PHPStan\Type\ObjectType; +use Rector\Rector\AbstractRector; +use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample; +use Symplify\RuleDocGenerator\ValueObject\RuleDefinition; +use Zenstruck\Foundry\Persistence\Proxy; + +// we're not using Rector's built-in rule RenameMethodRector because it does not support NullsafeMethodCall +final class ChangeProxyMethodCalls extends AbstractRector +{ + public function getRuleDefinition(): RuleDefinition + { + return new RuleDefinition( + 'Change from deprecated proxy methods to new methods.', + [ + new CodeSample( + <<<'CODE_SAMPLE' + $proxy->object(); + $proxy->save(); + $proxy->remove(); + $proxy->refresh(); + $proxy->forceSet(); + $proxy->forceGet(); + $proxy->repository(); + $proxy->enableAutoRefresh(); + $proxy->disableAutoRefresh(); + $proxy->withoutAutoRefresh(); + CODE_SAMPLE, + <<<'CODE_SAMPLE' + $proxy->_real(); + $proxy->_save(); + $proxy->_delete(); + $proxy->_refresh(); + $proxy->_set(); + $proxy->_get(); + $proxy->_repository(); + $proxy->_enableAutoRefresh(); + $proxy->_disableAutoRefresh(); + $proxy->_withoutAutoRefresh(); + CODE_SAMPLE + ), + ] + ); + } + + /** + * @return array> + */ + public function getNodeTypes(): array + { + return [Node\Expr\MethodCall::class, Node\Expr\NullsafeMethodCall::class]; + } + + /** + * @param Node\Expr\MethodCall|Node\Expr\NullsafeMethodCall $node + */ + public function refactor(Node $node): ?Node + { + if (!$this->isObjectType($node->var, new ObjectType(Proxy::class)) && !$this->isObjectType($node->var, new ObjectType(\Zenstruck\Foundry\Proxy::class))) { + return null; + } + + switch ($this->getName($node->name)) { + case 'object': + $node->name = new Node\Identifier('_real'); + + return $node; + case 'save': + $node->name = new Node\Identifier('_save'); + + return $node; + case 'remove': + $node->name = new Node\Identifier('_delete'); + + return $node; + case 'refresh': + $node->name = new Node\Identifier('_refresh'); + + return $node; + case 'forceSet': + $node->name = new Node\Identifier('_set'); + + return $node; + case 'forceGet': + $node->name = new Node\Identifier('_get'); + + return $node; + case 'repository': + $node->name = new Node\Identifier('_repository'); + + return $node; + case 'enableAutoRefresh': + $node->name = new Node\Identifier('_enableAutoRefresh'); + + return $node; + case 'disableAutoRefresh': + $node->name = new Node\Identifier('_disableAutoRefresh'); + + return $node; + case 'withoutAutoRefresh': + $node->name = new Node\Identifier('_withoutAutoRefresh'); + + return $node; + default: return null; + } + } +} diff --git a/utils/rector/src/ChangeStaticFactoryFakerCalls.php b/utils/rector/src/ChangeStaticFactoryFakerCalls.php new file mode 100644 index 000000000..6e046fb51 --- /dev/null +++ b/utils/rector/src/ChangeStaticFactoryFakerCalls.php @@ -0,0 +1,83 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Utils\Rector; + +use PhpParser\Node; +use PHPStan\Analyser\MutatingScope; +use Rector\Rector\AbstractRector; +use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample; +use Symplify\RuleDocGenerator\ValueObject\RuleDefinition; +use Zenstruck\Foundry\Factory; + +final class ChangeStaticFactoryFakerCalls extends AbstractRector +{ + /** + * @return array> + */ + public function getNodeTypes(): array + { + return [Node\Expr\StaticCall::class]; + } + + public function getRuleDefinition(): RuleDefinition + { + return new RuleDefinition( + 'Change Factory::faker() calls, outside from a factory (method is not protected.', + [ + new CodeSample( + <<<'CODE_SAMPLE' + // not in a factory class + Factory::faker(); + CODE_SAMPLE, + <<<'CODE_SAMPLE' + // not in a factory class + \Zenstruck\Foundry\faker(); + CODE_SAMPLE + ), + ] + ); + } + + /** + * @param Node\Expr\StaticCall $node + */ + public function refactor(Node $node): Node|int|null + { + // if method name is not faker, then do nothing + if ('faker' !== $this->getName($node->name)) { + return null; + } + + // if method is not called on a Factory class, then do nothing + if ( + !$node->class instanceof Node\Name + || !\is_a((string) $node->class, Factory::class, allow_string: true) + ) { + return null; + } + + /** @var MutatingScope $mutatingScope */ + $mutatingScope = $node->getAttribute('scope'); + + // if the Factory::faker() was called from a Factory, then do nothing + if ( + null !== ($classReflection = $mutatingScope->getClassReflection()) + && \is_a($classReflection->getName(), Factory::class, allow_string: true) + ) { + return null; + } + + return new Node\Expr\FuncCall(new Node\Name('\Zenstruck\Foundry\faker')); + } +} diff --git a/utils/rector/src/FoundrySetList.php b/utils/rector/src/FoundrySetList.php new file mode 100644 index 000000000..7a1d071b0 --- /dev/null +++ b/utils/rector/src/FoundrySetList.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Utils\Rector; + +use Rector\Set\Contract\SetListInterface; + +final class FoundrySetList implements SetListInterface +{ + /** @var string */ + public const UP_TO_FOUNDRY_2 = __DIR__.'/../config/foundry-set.php'; +} diff --git a/utils/rector/src/PersistenceResolver.php b/utils/rector/src/PersistenceResolver.php new file mode 100644 index 000000000..c355700fa --- /dev/null +++ b/utils/rector/src/PersistenceResolver.php @@ -0,0 +1,131 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Utils\Rector; + +use Doctrine\ODM\MongoDB\DocumentManager; +use Doctrine\ODM\MongoDB\Mapping\Annotations\Document; +use Doctrine\ODM\MongoDB\Mapping\Annotations\MappedSuperclass; +use Doctrine\ODM\MongoDB\Repository\DocumentRepository; +use Doctrine\ORM\EntityRepository; +use Doctrine\Persistence\ObjectRepository; +use PHPStan\Type\Doctrine\ObjectMetadataResolver; +use Zenstruck\Foundry\ModelFactory; +use Zenstruck\Foundry\ObjectFactory; + +/** + * This class will guess if a target class is persisted. + * This leverages phpstan/phpstan-doctrine. + * + * By default it only uses AttributeDriver or AnnotationDriver, but one can provide an "objectManagerLoader" + * which can read any Doctrine's mapping (even the ones declared outside the entities) + * + * @see https://github.com/phpstan/phpstan-doctrine#configuration + */ +final class PersistenceResolver +{ + /** @var array, class-string> */ + private array $factoryToTargetClass = []; + + private ObjectMetadataResolver $doctrineMetadataResolver; + + public function __construct(?string $objectManagerLoader = null) + { + $this->doctrineMetadataResolver = new ObjectMetadataResolver($objectManagerLoader, \sys_get_temp_dir().'/rector-doctrine'); // @phpstan-ignore-line + } + + /** @param class-string $factoryClass */ + public function shouldTransformFactoryIntoObjectFactory(string $factoryClass): bool + { + $targetClass = $this->targetClass($factoryClass); + + return !$this->shouldUseProxyFactory($targetClass); + } + + /** + * @param class-string $targetClass + */ + public function shouldUseProxyFactory(string $targetClass): bool + { + return !(new \ReflectionClass($targetClass))->isFinal() && $this->isPersisted($targetClass); + } + + /** + * @param class-string $targetClass + */ + public function isMongoDocument(string $targetClass): bool + { + // phpstan/phpstan-doctrine does not allow to determine if a class is managed by ODM + // so let's do a "best effort" et check if the class has a doctrine attribute + return (new \ReflectionClass($targetClass))->getAttributes(Document::class) + || (new \ReflectionClass($targetClass))->getAttributes(MappedSuperclass::class); + } + + /** + * @param class-string $factoryClass + * @return class-string + */ + public function targetClass(string $factoryClass): string + { + return $this->factoryToTargetClass[$factoryClass] ??= (static function() use ($factoryClass) { + return \is_a($factoryClass, ModelFactory::class, allow_string: true) + ? (new \ReflectionClass($factoryClass))->getMethod('getClass')->invoke(null) + : $factoryClass::class(); + })(); + } + + /** + * @param class-string $targetClass + * @return class-string + */ + public function geRepositoryClass($targetClass): string + { + $mongoAttribute = $this->getMongoAttribute($targetClass); + + if ($mongoAttribute) { + return $mongoAttribute->repositoryClass ?? DocumentRepository::class; // @phpstan-ignore-line + } + + return $this->doctrineMetadataResolver->getClassMetadata($targetClass)?->customRepositoryClassName ?? EntityRepository::class; // @phpstan-ignore-line + } + + /** + * @param class-string $targetClass + */ + private function isPersisted(string $targetClass): bool + { + $isPersisted = (bool) $this->doctrineMetadataResolver->getClassMetadata($targetClass); // @phpstan-ignore-line + + if ($isPersisted || !\class_exists(DocumentManager::class)) { + return $isPersisted; + } + + return $this->isMongoDocument($targetClass); + } + + /** + * @param class-string $targetClass + */ + private function getMongoAttribute(string $targetClass): MappedSuperclass|Document|null + { + $reflectionClass = new \ReflectionClass($targetClass); + + $attributes = $reflectionClass->getAttributes(Document::class) ?: $reflectionClass->getAttributes(MappedSuperclass::class); + + if (!$attributes) { + return null; + } + + return $attributes[0]->newInstance(); + } +} diff --git a/utils/rector/src/RemoveProxyRealObjectMethodCallsForNotProxifiedObjects.php b/utils/rector/src/RemoveProxyRealObjectMethodCallsForNotProxifiedObjects.php new file mode 100644 index 000000000..f80a897c7 --- /dev/null +++ b/utils/rector/src/RemoveProxyRealObjectMethodCallsForNotProxifiedObjects.php @@ -0,0 +1,167 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Utils\Rector; + +use PhpParser\Node; +use PHPStan\Analyser\MutatingScope; +use PHPStan\Type\Generic\GenericObjectType; +use PHPStan\Type\NullType; +use PHPStan\Type\ObjectType; +use PHPStan\Type\UnionType; +use Rector\Rector\AbstractRector; +use Rector\StaticTypeMapper\ValueObject\Type\FullyQualifiedObjectType; +use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample; +use Symplify\RuleDocGenerator\ValueObject\RuleDefinition; +use Zenstruck\Foundry\Persistence\Proxy; + +final class RemoveProxyRealObjectMethodCallsForNotProxifiedObjects extends AbstractRector +{ + private PersistenceResolver $persistenceResolver; + + public function __construct( + ?PersistenceResolver $persistenceResolver, + ) { + $this->persistenceResolver = $persistenceResolver ?? new PersistenceResolver(); + } + + public function getRuleDefinition(): RuleDefinition + { + return new RuleDefinition( + 'Remove `->object()`/`->_real()` calls on objects created by `ObjectFactory`.', + [ + new CodeSample( + <<<'CODE_SAMPLE' + SomeObjectFactory::new()->create()->object(); + SomeObjectFactory::new()->create()->_real(); + CODE_SAMPLE, + <<<'CODE_SAMPLE' + SomeObjectFactory::new()->create(); + SomeObjectFactory::new()->create(); + CODE_SAMPLE + ), + ] + ); + } + + /** + * @return array> + */ + public function getNodeTypes(): array + { + return [Node\Expr\MethodCall::class, Node\Expr\NullsafeMethodCall::class]; + } + + /** + * @param Node\Expr\MethodCall|Node\Expr\NullsafeMethodCall $node + */ + public function refactor(Node $node): ?Node + { + if (!\in_array($this->getName($node->name), ['object', '_real'], true)) { + return null; + } + + if ($node->var instanceof Node\Expr\FuncCall) { + $name = $node->var->name->getAttribute('namespacedName') ?? $this->getName($node->var->name); + if (\str_starts_with($name, '\\')) { + $name = \mb_substr($name, 1); + } + + if (\in_array($name, ['Zenstruck\Foundry\create', 'Zenstruck\Foundry\instantiate', 'Zenstruck\Foundry\Persistence\proxy'])) { + return null; + } + } + + /** + * If "object()" or "_real()" is called on an object which is a proxy, + * we should check if this object will use `ObjectFactory` as factory. + * If it does, we must remove the call. + */ + if ($this->isProxyOfFutureObjectFactory($node->var)) { + return $node->var; + } + + /** + * If "object()" or "_real()" is called on an object which is not a proxy, + * we MAY have already changed the base factory class. + * Then, if the method does not exist on the object's class, it is very likely we can safely remove the method call. + */ + if ($this->isRegularObjectWithoutGivenMethod($node)) { + return $node->var; + } + + return null; + } + + /** + * We only consider two cases: + * - proxy is nullable (ie: UnionType with a NullType) + * - proxy is a GenericObjectType + * + * Complex cases are not handled here. + */ + private function isProxyOfFutureObjectFactory(Node\Expr $var): bool + { + // not a proxy + if (!$this->isObjectType($var, new ObjectType(Proxy::class)) && !$this->isObjectType( + $var, + new ObjectType(\Zenstruck\Foundry\Proxy::class) + )) { + return false; + } + + /** @var MutatingScope $mutatingScope */ + $mutatingScope = $var->getAttribute('scope'); + + $type = $mutatingScope->getType($var); + + // Proxy is nullable: we extract the proxy type + if ($type instanceof UnionType) { + $types = $type->getTypes(); + + if (\count($types) > 2) { + return false; + } + + if ($types[0] instanceof NullType) { + $type = $types[1]; + } elseif ($types[1] instanceof NullType) { + $type = $types[0]; + } else { + return false; + } + } + + return $type instanceof GenericObjectType + && 1 === \count($types = $type->getTypes()) + && $types[0] instanceof ObjectType + && !$this->persistenceResolver->shouldUseProxyFactory($types[0]->getClassName()); // @phpstan-ignore-line + } + + private function isRegularObjectWithoutGivenMethod(Node\Expr\MethodCall|Node\Expr\NullsafeMethodCall $node): bool + { + $type = $this->getType($node->var); + + if (!$type instanceof FullyQualifiedObjectType) { + return false; + } + + try { + (new \ReflectionClass($type->getClassName()))->getMethod($this->getName($node->name) ?? ''); // @phpstan-ignore-line + + return false; + } catch (\ReflectionException) { + return true; + } + } +} diff --git a/utils/rector/src/RemoveUnproxifyArrayMap.php b/utils/rector/src/RemoveUnproxifyArrayMap.php new file mode 100644 index 000000000..4dd11c9d0 --- /dev/null +++ b/utils/rector/src/RemoveUnproxifyArrayMap.php @@ -0,0 +1,152 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Utils\Rector; + +use PhpParser\Node; +use PhpParser\Node\FunctionLike; +use PHPStan\Type\ArrayType; +use PHPStan\Type\ClosureType; +use PHPStan\Type\Generic\GenericObjectType; +use PHPStan\Type\TypeWithClassName; +use Rector\Rector\AbstractRector; +use Rector\StaticTypeMapper\ValueObject\Type\FullyQualifiedObjectType; +use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample; +use Symplify\RuleDocGenerator\ValueObject\RuleDefinition; +use Zenstruck\Foundry\Persistence\Proxy; + +final class RemoveUnproxifyArrayMap extends AbstractRector +{ + public function __construct( + private PersistenceResolver $persistenceResolver, + ) { + } + + public function getRuleDefinition(): RuleDefinition + { + return new RuleDefinition( + 'Remove useless array_map() which calls ->object() on proxies list.', + [ + new CodeSample( + <<<'CODE_SAMPLE' + array_map(fn(Proxy $proxy) => $proxy->object, ObjectFactory::createMany()) + CODE_SAMPLE, + <<<'CODE_SAMPLE' + ObjectFactory::createMany() + CODE_SAMPLE + ), + ] + ); + } + + /** + * @return array> + */ + public function getNodeTypes(): array + { + return [Node\Expr\FuncCall::class]; + } + + /** + * @param Node\Expr\FuncCall $node + */ + public function refactor(Node $node): ?Node + { + if ('array_map' !== $this->getName($node->name)) { + return null; + } + + if (2 !== \count($node->args)) { + return null; + } + + // if the callable looks like "fn(Proxy $p) => $p->object()" + if (!$this->isCallableUnproxify($node)) { + return null; + } + + // and the param is an array of objects which are NOT proxies + if (!$this->isArrayMapTargetingNonProxyObject($node)) { + return null; + } + + // then replace the array_map by it's array param + return $node->getArgs()[1]->value; + } + + private function isCallableUnproxify(Node\Expr\FuncCall $node): bool + { + $callable = $node->getArgs()[0]->value; + + if (!$this->getType($callable) instanceof ClosureType) { + return false; // first argument can be any type of callable, but let's only handle closures + } + + if (!$callable instanceof FunctionLike) { + return false; // at this point this shoudl not happend + } + + if (1 !== \count($callable->getParams())) { + return false; // let's only handle callables with one param + } + + $paramType = $this->getType($callable->getParams()[0]); + + if (!$paramType instanceof FullyQualifiedObjectType || !\is_a($paramType->getClassName(), Proxy::class, allow_string: true)) { + return false; // let's only handle param when it is fully typed as a Proxy + } + + // assert the body of the callable is a single ->object() / ->_real() call on its unique param + return 1 === \count($callable->getStmts() ?? []) + && ($return = $callable->getStmts()[0]) instanceof Node\Stmt\Return_ + && ($methodCall = $return->expr) instanceof Node\Expr\MethodCall + && $this->getName($methodCall->var) === $this->getName($callable->getParams()[0]->var) + && \in_array($this->getName($methodCall->name), ['_real', 'object']) + && 0 === \count($methodCall->args) + ; + } + + private function isArrayMapTargetingNonProxyObject(Node\Expr\FuncCall $node): bool + { + $array = $this->getType($node->getArgs()[1]->value); + + if (!$array instanceof ArrayType) { + return false; // this should not happen: second argument of an array_map call IS an array + } + + $iterableType = $array->getIterableValueType(); + if (!$iterableType instanceof TypeWithClassName) { + // if it is not a TypeWithClassName, we could be in on of these situations, which we cannot handle + // - the parameter is badly typed + // - the iterable type is not an object + // - we have a more complex type + return false; + } + + $className = $iterableType->getClassName(); + + if (!\is_a($className, Proxy::class, allow_string: true)) { + return true; // the objects in array param are NOT a proxy, we should make the replacement + } + + // otherwise, let's check if we have a Proxy with Object is not persistable. + + if (!$iterableType instanceof GenericObjectType) { + return false; // not a fully typed generic, cannot guess if Object is persistable + } + + return 1 === \count($iterableType->getTypes()) + && ($genericType = $iterableType->getTypes()[0]) instanceof TypeWithClassName + && !$this->persistenceResolver->shouldUseProxyFactory($genericType->getClassName()); // @phpstan-ignore-line + } +} diff --git a/utils/rector/src/RuleRequirementsChecker.php b/utils/rector/src/RuleRequirementsChecker.php new file mode 100644 index 000000000..3f33461df --- /dev/null +++ b/utils/rector/src/RuleRequirementsChecker.php @@ -0,0 +1,31 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Utils\Rector; + +use PHPStan\Type\Doctrine\ObjectMetadataResolver; +use Rector\Rector\AbstractRector; + +final class RuleRequirementsChecker +{ + public static function checkRequirements(): void + { + if (!\class_exists(AbstractRector::class)) { + throw new \RuntimeException('Foundry\'s Rector rules need package rector/rector to be at least at version 1.0. Please update it with command "composer update rector/rector"'); + } + + if (!\class_exists(ObjectMetadataResolver::class)) { + throw new \RuntimeException('Foundry\'s Rector rules need package phpstan/phpstan-doctrine. Please install it with command "composer require phpstan/phpstan-doctrine --dev"'); + } + } +} diff --git a/utils/rector/tests/AddProxyToFactoryCollectionTypeInPhpDoc/AddProxyToFactoryCollectionTypeInPhpDocTest.php b/utils/rector/tests/AddProxyToFactoryCollectionTypeInPhpDoc/AddProxyToFactoryCollectionTypeInPhpDocTest.php new file mode 100644 index 000000000..e4e55ad5c --- /dev/null +++ b/utils/rector/tests/AddProxyToFactoryCollectionTypeInPhpDoc/AddProxyToFactoryCollectionTypeInPhpDocTest.php @@ -0,0 +1,39 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Utils\Rector\Tests\AddProxyToFactoryCollectionTypeInPhpDoc; + +use Rector\Testing\PHPUnit\AbstractRectorTestCase; + +final class AddProxyToFactoryCollectionTypeInPhpDocTest extends AbstractRectorTestCase +{ + /** + * @dataProvider provideData + * + * @test + */ + public function test(string $filePath): void + { + $this->doTestFile($filePath); + } + + public static function provideData(): \Iterator + { + return self::yieldFilesFromDirectory(__DIR__.'/Fixtures'); + } + + public function provideConfigFilePath(): string + { + return __DIR__.'/config.php'; + } +} diff --git a/utils/rector/tests/AddProxyToFactoryCollectionTypeInPhpDoc/Fixtures/add-proxy-generic-type.php.inc b/utils/rector/tests/AddProxyToFactoryCollectionTypeInPhpDoc/Fixtures/add-proxy-generic-type.php.inc new file mode 100644 index 000000000..78b241969 --- /dev/null +++ b/utils/rector/tests/AddProxyToFactoryCollectionTypeInPhpDoc/Fixtures/add-proxy-generic-type.php.inc @@ -0,0 +1,33 @@ + $factoryCollection + */ + public function foo(FactoryCollection $factoryCollection) + { + } +} + +?> +----- +> $factoryCollection + */ + public function foo(FactoryCollection $factoryCollection) + { + } +} + +?> diff --git a/utils/rector/tests/AddProxyToFactoryCollectionTypeInPhpDoc/Fixtures/remove-proxy-generic-type.php.inc b/utils/rector/tests/AddProxyToFactoryCollectionTypeInPhpDoc/Fixtures/remove-proxy-generic-type.php.inc new file mode 100644 index 000000000..c49f697c0 --- /dev/null +++ b/utils/rector/tests/AddProxyToFactoryCollectionTypeInPhpDoc/Fixtures/remove-proxy-generic-type.php.inc @@ -0,0 +1,51 @@ +> $factoryCollection + */ + public function foo(FactoryCollection $factoryCollection) + { + } + + /** + * @param FactoryCollection> $factoryCollection + */ + public function bar(FactoryCollection $factoryCollection) + { + } +} + +?> +----- + $factoryCollection + */ + public function foo(FactoryCollection $factoryCollection) + { + } + + /** + * @param FactoryCollection<\Zenstruck\Foundry\Utils\Rector\Tests\Fixtures\DummyObject> $factoryCollection + */ + public function bar(FactoryCollection $factoryCollection) + { + } +} + +?> diff --git a/utils/rector/tests/AddProxyToFactoryCollectionTypeInPhpDoc/Fixtures/skip-if-not-persistent-type.php.inc b/utils/rector/tests/AddProxyToFactoryCollectionTypeInPhpDoc/Fixtures/skip-if-not-persistent-type.php.inc new file mode 100644 index 000000000..2daa8b943 --- /dev/null +++ b/utils/rector/tests/AddProxyToFactoryCollectionTypeInPhpDoc/Fixtures/skip-if-not-persistent-type.php.inc @@ -0,0 +1,16 @@ + $factoryCollection + */ + public function foo(FactoryCollection $factoryCollection) + { + } +} + +?> diff --git a/utils/rector/tests/AddProxyToFactoryCollectionTypeInPhpDoc/config.php b/utils/rector/tests/AddProxyToFactoryCollectionTypeInPhpDoc/config.php new file mode 100644 index 000000000..fbc8579ba --- /dev/null +++ b/utils/rector/tests/AddProxyToFactoryCollectionTypeInPhpDoc/config.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Rector\Config\RectorConfig; +use Zenstruck\Foundry\Utils\Rector\AddProxyToFactoryCollectionTypeInPhpDoc; + +return static function(RectorConfig $rectorConfig): void { + $rectorConfig->rule(AddProxyToFactoryCollectionTypeInPhpDoc::class); +}; diff --git a/utils/rector/tests/AllRules/AllRulesTest.php b/utils/rector/tests/AllRules/AllRulesTest.php new file mode 100644 index 000000000..095ded485 --- /dev/null +++ b/utils/rector/tests/AllRules/AllRulesTest.php @@ -0,0 +1,39 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Utils\Rector\Tests\AddProxyToFactoryCollectionTypeInPhpDoc; + +use Rector\Testing\PHPUnit\AbstractRectorTestCase; + +final class AllRulesTest extends AbstractRectorTestCase +{ + /** + * @dataProvider provideData + * + * @test + */ + public function test(string $filePath): void + { + $this->doTestFile($filePath); + } + + public static function provideData(): \Iterator + { + return self::yieldFilesFromDirectory(__DIR__.'/Fixtures'); + } + + public function provideConfigFilePath(): string + { + return __DIR__.'/config.php'; + } +} diff --git a/utils/rector/tests/AllRules/Fixtures/object-factory.php.inc b/utils/rector/tests/AllRules/Fixtures/object-factory.php.inc new file mode 100644 index 000000000..5c1f4a2d0 --- /dev/null +++ b/utils/rector/tests/AllRules/Fixtures/object-factory.php.inc @@ -0,0 +1,92 @@ + + * + * @method DummyObject|Proxy create(array|callable $attributes = []) + * @method static DummyObject|Proxy createOne(array $attributes = []) + * @method static DummyObject[]|Proxy[] createMany(int $number, array|callable $attributes = []) + * @method static DummyObject[]|Proxy[] createSequence(iterable|callable $sequence) + */ +final class DummyObjectModelFactory extends ModelFactory +{ + protected function getDefaults(): array + { + return ['field' => self::faker()]; + } + + public function published(): static + { + return $this->addState(['published' => true]); + } + + protected function initialize(): self + { + return $this + ->withoutPersisting() + ->instantiateWith( + static fn() => (new Instantiator()) + ->allowExtraAttributes(['some', 'fields']) + ->alwaysForceProperties(['other', 'fields']) + ->allowExtraAttributes() + ->alwaysForceProperties() + ); + } + + protected static function getClass(): string + { + return DummyObject::class; + } +} + +?> +----- + + */ +final class DummyObjectModelFactory extends ObjectFactory +{ + protected function defaults(): array + { + return ['field' => self::faker()]; + } + + public function published(): static + { + return $this->with(['published' => true]); + } + + protected function initialize(): static + { + return $this + ->instantiateWith( + static fn() => (Instantiator::withoutConstructor()) + ->allowExtra(...['some', 'fields']) + ->alwaysForce(...['other', 'fields']) + ->allowExtra() + ->alwaysForce() + ); + } + + public static function class(): string + { + return DummyObject::class; + } +} + +?> diff --git a/utils/rector/tests/AllRules/Fixtures/proxy-factory.php.inc b/utils/rector/tests/AllRules/Fixtures/proxy-factory.php.inc new file mode 100644 index 000000000..2b642cac7 --- /dev/null +++ b/utils/rector/tests/AllRules/Fixtures/proxy-factory.php.inc @@ -0,0 +1,81 @@ + + * + * @method static RepositoryProxy repository() + * @method DummyPersistentObject|Proxy create(array|callable $attributes = []) + * + * @phpstan-method Proxy create(array|callable $attributes = []) + * @phpstan-method static Proxy createOne(array $attributes = []) + * @phpstan-method static RepositoryProxy repository() + */ +final class DummyPersistentModelFactory extends ModelFactory +{ + protected function getDefaults(): array + { + return []; + } + + protected function initialize(): self + { + return $this + ->withoutPersisting(); + } + + protected static function getClass(): string + { + return DummyPersistentObject::class; + } +} + +?> +----- + + * + * @method static ProxyRepositoryDecorator repository() + * @method DummyPersistentObject|Proxy create(array|callable $attributes = []) + * + * @phpstan-method DummyPersistentObject&Proxy create(array|callable $attributes = []) + * @phpstan-method static DummyPersistentObject&Proxy createOne(array $attributes = []) + * @phpstan-method static ProxyRepositoryDecorator repository() + */ +final class DummyPersistentModelFactory extends PersistentProxyObjectFactory +{ + protected function defaults(): array + { + return []; + } + + protected function initialize(): static + { + return $this + ->withoutPersisting(); + } + + public static function class(): string + { + return DummyPersistentObject::class; + } +} + +?> diff --git a/utils/rector/tests/AllRules/Fixtures/test-case.php.inc b/utils/rector/tests/AllRules/Fixtures/test-case.php.inc new file mode 100644 index 000000000..33865a6b2 --- /dev/null +++ b/utils/rector/tests/AllRules/Fixtures/test-case.php.inc @@ -0,0 +1,129 @@ +enablePersist(); + $this->disablePersist(); + } + + /** + * @param FactoryCollection $factoryCollectionWithProxy + * @param FactoryCollection $factoryCollectionWithoutProxy + */ + public function testSomething(FactoryCollection $factoryCollectionWithProxy, FactoryCollection $factoryCollectionWithoutProxy): void + { + DummyObjectModelFactory::new()->withAttributes(['published' => true])->object(); + DummyPersistentModelFactory::createOne()->object(); + + DummyPersistentModelFactory::new()->many(0, 1)->factory(); + + /** @var Proxy */ + $object = DummyPersistentModelFactory::createOne(); + $object->refresh(); + + \Zenstruck\Foundry\create(SomeClass::class, []); + \Zenstruck\Foundry\instantiate(SomeClass::class, ['published' => true]); + \Zenstruck\Foundry\repository($object); + + \Zenstruck\Foundry\Factory::delayFlush(static fn() => true); + \Zenstruck\Foundry\Test\TestState::configure(faker: null); + + Factory::faker(); + + FactoryCollection::set(DummyObjectModelFactory::new(), 5); + + $unproxified1 = array_map(static fn (Proxy $proxy) => $proxy->object(), DummyObjectModelFactory::createMany(5)); + $unproxified2 = array_map(static fn (Proxy $proxy) => $proxy->object(), DummyObjectModelFactory::new()->many(5)->create()); + $unproxified3 = array_map(static fn (Proxy $proxy) => $proxy->object(), DummyPersistentModelFactory::createMany(5)); + $unproxified4 = array_map(static fn (Proxy $proxy) => $proxy->object(), DummyPersistentModelFactory::new()->many(5)->create()); + + instantiate(DummyPersistentObject::class, [])->object(); + instantiate(DummyObject::class, [])->object(); + } +} + +?> +----- +> $factoryCollectionWithProxy + * @param FactoryCollection $factoryCollectionWithoutProxy + */ + public function testSomething(FactoryCollection $factoryCollectionWithProxy, FactoryCollection $factoryCollectionWithoutProxy): void + { + DummyObjectModelFactory::new()->with(['published' => true]); + DummyPersistentModelFactory::createOne()->_real(); + + DummyPersistentModelFactory::new()->many(0, 1)->factory; + + /** @var Proxy */ + $object = DummyPersistentModelFactory::createOne(); + $object->_refresh(); + + persist_proxy(SomeClass::class, []); + \Zenstruck\Foundry\Persistence\proxy(object(SomeClass::class, ['published' => true])); + repository($object); + + flush_after(static fn() => true); + UnitTestConfig::configure(faker: null); + + \Zenstruck\Foundry\faker(); + + FactoryCollection::many(DummyObjectModelFactory::new(), 5); + + $unproxified1 = DummyObjectModelFactory::createMany(5); + $unproxified2 = DummyObjectModelFactory::new()->many(5)->create(); + $unproxified3 = array_map(static fn (Proxy $proxy) => $proxy->_real(), DummyPersistentModelFactory::createMany(5)); + $unproxified4 = array_map(static fn (Proxy $proxy) => $proxy->_real(), DummyPersistentModelFactory::new()->many(5)->create()); + + \Zenstruck\Foundry\Persistence\proxy(object(DummyPersistentObject::class, []))->_real(); + \Zenstruck\Foundry\Persistence\proxy(object(DummyObject::class, []))->_real(); + } +} + +?> diff --git a/utils/rector/tests/AllRules/config.php b/utils/rector/tests/AllRules/config.php new file mode 100644 index 000000000..56185eb5f --- /dev/null +++ b/utils/rector/tests/AllRules/config.php @@ -0,0 +1,23 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Rector\Config\RectorConfig; +use Zenstruck\Foundry\Utils\Rector\FoundrySetList; + +return static function(RectorConfig $rectorConfig): void { + $rectorConfig->removeUnusedImports(); + $rectorConfig->importNames(); + $rectorConfig->importShortClasses(false); + + $rectorConfig->sets([FoundrySetList::UP_TO_FOUNDRY_2]); +}; diff --git a/utils/rector/tests/ChangeDisableEnablePersist/ChangeDisableEnablePersistTest.php b/utils/rector/tests/ChangeDisableEnablePersist/ChangeDisableEnablePersistTest.php new file mode 100644 index 000000000..6f2966d63 --- /dev/null +++ b/utils/rector/tests/ChangeDisableEnablePersist/ChangeDisableEnablePersistTest.php @@ -0,0 +1,39 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Utils\Rector\Tests\ChangeDisableEnablePersist; + +use Rector\Testing\PHPUnit\AbstractRectorTestCase; + +final class ChangeDisableEnablePersistTest extends AbstractRectorTestCase +{ + /** + * @dataProvider provideData + * + * @test + */ + public function test(string $filePath): void + { + $this->doTestFile($filePath); + } + + public static function provideData(): \Iterator + { + return self::yieldFilesFromDirectory(__DIR__.'/Fixtures'); + } + + public function provideConfigFilePath(): string + { + return __DIR__.'/config.php'; + } +} diff --git a/utils/rector/tests/ChangeDisableEnablePersist/Fixtures/change-legacy-function-when-in-kernel-test-case.php.inc b/utils/rector/tests/ChangeDisableEnablePersist/Fixtures/change-legacy-function-when-in-kernel-test-case.php.inc new file mode 100644 index 000000000..5bad7efc1 --- /dev/null +++ b/utils/rector/tests/ChangeDisableEnablePersist/Fixtures/change-legacy-function-when-in-kernel-test-case.php.inc @@ -0,0 +1,43 @@ +enablePersist(); + $this->disablePersist(); + } +} + +?> +----- + diff --git a/utils/rector/tests/ChangeDisableEnablePersist/Fixtures/remove-legacy-function-when-in-unit-test.php.inc b/utils/rector/tests/ChangeDisableEnablePersist/Fixtures/remove-legacy-function-when-in-unit-test.php.inc new file mode 100644 index 000000000..d4b29a45e --- /dev/null +++ b/utils/rector/tests/ChangeDisableEnablePersist/Fixtures/remove-legacy-function-when-in-unit-test.php.inc @@ -0,0 +1,41 @@ +enablePersist(); + $this->disablePersist(); + } +} + +?> +----- + diff --git a/utils/rector/tests/ChangeDisableEnablePersist/config.php b/utils/rector/tests/ChangeDisableEnablePersist/config.php new file mode 100644 index 000000000..a19273d6f --- /dev/null +++ b/utils/rector/tests/ChangeDisableEnablePersist/config.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Rector\Config\RectorConfig; +use Zenstruck\Foundry\Utils\Rector\ChangeDisableEnablePersist; + +return static function(RectorConfig $rectorConfig): void { + $rectorConfig->rule(ChangeDisableEnablePersist::class); +}; diff --git a/utils/rector/tests/ChangeFactoryBaseClass/ChangeFactoryBaseClassTest.php b/utils/rector/tests/ChangeFactoryBaseClass/ChangeFactoryBaseClassTest.php new file mode 100644 index 000000000..84ed949b2 --- /dev/null +++ b/utils/rector/tests/ChangeFactoryBaseClass/ChangeFactoryBaseClassTest.php @@ -0,0 +1,39 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Utils\Rector\Tests\ChangeFactoryBaseClass; + +use Rector\Testing\PHPUnit\AbstractRectorTestCase; + +final class ChangeFactoryBaseClassTest extends AbstractRectorTestCase +{ + /** + * @dataProvider provideData + * + * @test + */ + public function test(string $filePath): void + { + $this->doTestFile($filePath); + } + + public static function provideData(): \Iterator + { + return self::yieldFilesFromDirectory(__DIR__.'/Fixtures'); + } + + public function provideConfigFilePath(): string + { + return __DIR__.'/config.php'; + } +} diff --git a/utils/rector/tests/ChangeFactoryBaseClass/ChangeFactoryBaseClassWithObjectManagerTest.php b/utils/rector/tests/ChangeFactoryBaseClass/ChangeFactoryBaseClassWithObjectManagerTest.php new file mode 100644 index 000000000..652fc0219 --- /dev/null +++ b/utils/rector/tests/ChangeFactoryBaseClass/ChangeFactoryBaseClassWithObjectManagerTest.php @@ -0,0 +1,39 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Utils\Rector\Tests\ChangeFactoryBaseClass; + +use Rector\Testing\PHPUnit\AbstractRectorTestCase; + +final class ChangeFactoryBaseClassWithObjectManagerTest extends AbstractRectorTestCase +{ + /** + * @dataProvider provideData + * + * @test + */ + public function test(string $filePath): void + { + $this->doTestFile($filePath); + } + + public static function provideData(): \Iterator + { + return self::yieldFilesFromDirectory(__DIR__.'/Fixtures'); + } + + public function provideConfigFilePath(): string + { + return __DIR__.'/config-with-object-manager.php'; + } +} diff --git a/utils/rector/tests/ChangeFactoryBaseClass/Fixtures/change-factory-extending-user-defined-factory.php.inc b/utils/rector/tests/ChangeFactoryBaseClass/Fixtures/change-factory-extending-user-defined-factory.php.inc new file mode 100644 index 000000000..65971103e --- /dev/null +++ b/utils/rector/tests/ChangeFactoryBaseClass/Fixtures/change-factory-extending-user-defined-factory.php.inc @@ -0,0 +1,53 @@ + +----- + diff --git a/utils/rector/tests/ChangeFactoryBaseClass/Fixtures/change-to-object-factory.php.inc b/utils/rector/tests/ChangeFactoryBaseClass/Fixtures/change-to-object-factory.php.inc new file mode 100644 index 000000000..5074df1ef --- /dev/null +++ b/utils/rector/tests/ChangeFactoryBaseClass/Fixtures/change-to-object-factory.php.inc @@ -0,0 +1,66 @@ + + * + * @method DummyObject|Proxy create(array|callable $attributes = []) + * @method static DummyObject|Proxy createOne(array $attributes = []) + * @method static DummyObject[]|Proxy[] createMany(int $number, array|callable $attributes = []) + * @method static DummyObject[]|Proxy[] createSequence(iterable|callable $sequence) + */ +final class DummyObjectModelFactory extends ModelFactory +{ + protected function getDefaults(): array + { + return []; + } + + protected function initialize(): self + { + return $this; + } + + protected static function getClass(): string + { + return DummyObject::class; + } +} + +?> +----- + + */ +final class DummyObjectModelFactory extends \Zenstruck\Foundry\ObjectFactory +{ + protected function defaults(): array + { + return []; + } + + protected function initialize(): static + { + return $this; + } + + public static function class(): string + { + return DummyObject::class; + } +} + +?> diff --git a/utils/rector/tests/ChangeFactoryBaseClass/Fixtures/change-to-proxy-factory.php.inc b/utils/rector/tests/ChangeFactoryBaseClass/Fixtures/change-to-proxy-factory.php.inc new file mode 100644 index 000000000..8c7ed749a --- /dev/null +++ b/utils/rector/tests/ChangeFactoryBaseClass/Fixtures/change-to-proxy-factory.php.inc @@ -0,0 +1,81 @@ + + * + * @method static DummyPersistentObject[] createMany(int $number, array|callable $attributes = []) + * @method static RepositoryProxy repository() + * @method DummyPersistentObject|Proxy create(array|callable $attributes = []) + * + * @phpstan-method Proxy&DummyPersistentObject create(array|callable $attributes = []) + * @phpstan-method static Proxy createOne(array $attributes = []) + * @phpstan-method static DummyPersistentObject[]&Proxy[] all() + * @phpstan-method static RepositoryProxy repository() + */ +final class DummyPersistentModelFactory extends ModelFactory +{ + protected function getDefaults(): array + { + return []; + } + + protected function initialize(): self + { + return $this; + } + + protected static function getClass(): string + { + return DummyPersistentObject::class; + } +} + +?> +----- + + * + * @method static \Zenstruck\Foundry\Utils\Rector\Tests\Fixtures\DummyPersistentObject[]|Proxy[] createMany(int $number, array|callable $attributes = []) + * @method static \Zenstruck\Foundry\Persistence\ProxyRepositoryDecorator<\Zenstruck\Foundry\Utils\Rector\Tests\Fixtures\DummyPersistentObject, \Doctrine\ORM\EntityRepository> repository() + * @method \Zenstruck\Foundry\Utils\Rector\Tests\Fixtures\DummyPersistentObject|Proxy create(array|callable $attributes = []) + * + * @phpstan-method \Zenstruck\Foundry\Utils\Rector\Tests\Fixtures\DummyPersistentObject&Proxy<\Zenstruck\Foundry\Utils\Rector\Tests\Fixtures\DummyPersistentObject> create(array|callable $attributes = []) + * @phpstan-method static \Zenstruck\Foundry\Utils\Rector\Tests\Fixtures\DummyPersistentObject&Proxy<\Zenstruck\Foundry\Utils\Rector\Tests\Fixtures\DummyPersistentObject> createOne(array $attributes = []) + * @phpstan-method static list<\Zenstruck\Foundry\Utils\Rector\Tests\Fixtures\DummyPersistentObject&Proxy<\Zenstruck\Foundry\Utils\Rector\Tests\Fixtures\DummyPersistentObject>> all() + * @phpstan-method static \Zenstruck\Foundry\Persistence\ProxyRepositoryDecorator<\Zenstruck\Foundry\Utils\Rector\Tests\Fixtures\DummyPersistentObject, \Doctrine\ORM\EntityRepository> repository() + */ +final class DummyPersistentModelFactory extends \Zenstruck\Foundry\Persistence\PersistentProxyObjectFactory +{ + protected function defaults(): array + { + return []; + } + + protected function initialize(): static + { + return $this; + } + + public static function class(): string + { + return DummyPersistentObject::class; + } +} + +?> diff --git a/utils/rector/tests/ChangeFactoryBaseClass/Fixtures/change-to-proxy-mongo-factory.php.inc b/utils/rector/tests/ChangeFactoryBaseClass/Fixtures/change-to-proxy-mongo-factory.php.inc new file mode 100644 index 000000000..186c76a34 --- /dev/null +++ b/utils/rector/tests/ChangeFactoryBaseClass/Fixtures/change-to-proxy-mongo-factory.php.inc @@ -0,0 +1,83 @@ + + * + * @method static RepositoryProxy repository() + * @method DummyPersistentDocument|Proxy create(array|callable $attributes = []) + * + * @phpstan-method Proxy&DummyPersistentDocument create(array|callable $attributes = []) + * @phpstan-method static Proxy createOne(array $attributes = []) + * @phpstan-method static DummyPersistentDocument[]&Proxy[] all() + * @phpstan-method static DummyPersistentDocument[]&Proxy[] createMany(int $number, array|callable $attributes = []) + * @phpstan-method static RepositoryProxy repository() + */ +final class DummyPersistentDocumentFactory extends ModelFactory +{ + protected function getDefaults(): array + { + return []; + } + + protected function initialize(): self + { + return $this; + } + + protected static function getClass(): string + { + return DummyPersistentDocument::class; + } +} + +?> +----- + + * + * @method static \Zenstruck\Foundry\Persistence\ProxyRepositoryDecorator<\Zenstruck\Foundry\Utils\Rector\Tests\Fixtures\DummyPersistentDocument, \Doctrine\ODM\MongoDB\Repository\DocumentRepository> repository() + * @method \Zenstruck\Foundry\Utils\Rector\Tests\Fixtures\DummyPersistentDocument|Proxy create(array|callable $attributes = []) + * + * @phpstan-method \Zenstruck\Foundry\Utils\Rector\Tests\Fixtures\DummyPersistentDocument&Proxy<\Zenstruck\Foundry\Utils\Rector\Tests\Fixtures\DummyPersistentDocument> create(array|callable $attributes = []) + * @phpstan-method static \Zenstruck\Foundry\Utils\Rector\Tests\Fixtures\DummyPersistentDocument&Proxy<\Zenstruck\Foundry\Utils\Rector\Tests\Fixtures\DummyPersistentDocument> createOne(array $attributes = []) + * @phpstan-method static list<\Zenstruck\Foundry\Utils\Rector\Tests\Fixtures\DummyPersistentDocument&Proxy<\Zenstruck\Foundry\Utils\Rector\Tests\Fixtures\DummyPersistentDocument>> all() + * @phpstan-method static list<\Zenstruck\Foundry\Utils\Rector\Tests\Fixtures\DummyPersistentDocument&Proxy<\Zenstruck\Foundry\Utils\Rector\Tests\Fixtures\DummyPersistentDocument>> createMany(int $number, array|callable $attributes = []) + * @phpstan-method static \Zenstruck\Foundry\Persistence\ProxyRepositoryDecorator<\Zenstruck\Foundry\Utils\Rector\Tests\Fixtures\DummyPersistentDocument, \Doctrine\ODM\MongoDB\Repository\DocumentRepository> repository() + */ +final class DummyPersistentDocumentFactory extends \Zenstruck\Foundry\Persistence\PersistentProxyObjectFactory +{ + protected function defaults(): array + { + return []; + } + + protected function initialize(): static + { + return $this; + } + + public static function class(): string + { + return DummyPersistentDocument::class; + } +} + +?> diff --git a/utils/rector/tests/ChangeFactoryBaseClass/config-with-object-manager.php b/utils/rector/tests/ChangeFactoryBaseClass/config-with-object-manager.php new file mode 100644 index 000000000..668aaf179 --- /dev/null +++ b/utils/rector/tests/ChangeFactoryBaseClass/config-with-object-manager.php @@ -0,0 +1,24 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Rector\Config\RectorConfig; +use Zenstruck\Foundry\Utils\Rector\ChangeFactoryBaseClass; +use Zenstruck\Foundry\Utils\Rector\PersistenceResolver; + +return static function(RectorConfig $rectorConfig): void { + $rectorConfig->singleton( + PersistenceResolver::class, + static fn() => new PersistenceResolver(__DIR__.'/entity-manager.php') + ); + $rectorConfig->rule(ChangeFactoryBaseClass::class); +}; diff --git a/utils/rector/tests/ChangeFactoryBaseClass/config.php b/utils/rector/tests/ChangeFactoryBaseClass/config.php new file mode 100644 index 000000000..d25ea549c --- /dev/null +++ b/utils/rector/tests/ChangeFactoryBaseClass/config.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Rector\Config\RectorConfig; +use Zenstruck\Foundry\Utils\Rector\ChangeFactoryBaseClass; + +return static function(RectorConfig $rectorConfig): void { + $rectorConfig->rule(ChangeFactoryBaseClass::class); +}; diff --git a/utils/rector/tests/ChangeFactoryBaseClass/entity-manager.php b/utils/rector/tests/ChangeFactoryBaseClass/entity-manager.php new file mode 100644 index 000000000..e3cd89d03 --- /dev/null +++ b/utils/rector/tests/ChangeFactoryBaseClass/entity-manager.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Doctrine\ORM\Configuration; +use Doctrine\ORM\EntityManager; +use Doctrine\ORM\Mapping\Driver\AttributeDriver; +use Doctrine\Persistence\Mapping\Driver\MappingDriverChain; +use Symfony\Component\Cache\Adapter\ArrayAdapter; + +$config = new Configuration(); +$config->setProxyDir(__DIR__); +$config->setProxyNamespace('Zenstruck\Foundry\Utils\Rector\Tests\ChangeFactoryBaseClass\OrmProxies'); +$config->setMetadataCache(new ArrayAdapter()); + +$metadataDriver = new MappingDriverChain(); +$metadataDriver->addDriver(new AttributeDriver([]), 'Zenstruck\\Foundry\\Utils\\Rector\\Tests\\Fixtures\\'); + +$config->setMetadataDriverImpl($metadataDriver); + +return EntityManager::create( + [ + 'driver' => 'pdo_sqlite', + 'memory' => true, + ], + $config +); diff --git a/utils/rector/tests/ChangeFactoryMethodCalls/ChangeFactoryMethodCallsTest.php b/utils/rector/tests/ChangeFactoryMethodCalls/ChangeFactoryMethodCallsTest.php new file mode 100644 index 000000000..74765da72 --- /dev/null +++ b/utils/rector/tests/ChangeFactoryMethodCalls/ChangeFactoryMethodCallsTest.php @@ -0,0 +1,39 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Utils\Rector\Tests\ChangeFactoryMethodCalls; + +use Rector\Testing\PHPUnit\AbstractRectorTestCase; + +final class ChangeFactoryMethodCallsTest extends AbstractRectorTestCase +{ + /** + * @dataProvider provideData + * + * @test + */ + public function test(string $filePath): void + { + $this->doTestFile($filePath); + } + + public static function provideData(): \Iterator + { + return self::yieldFilesFromDirectory(__DIR__.'/Fixtures'); + } + + public function provideConfigFilePath(): string + { + return __DIR__.'/config.php'; + } +} diff --git a/utils/rector/tests/ChangeFactoryMethodCalls/Fixtures/change-factory-extending-user-defined-factory.php.inc b/utils/rector/tests/ChangeFactoryMethodCalls/Fixtures/change-factory-extending-user-defined-factory.php.inc new file mode 100644 index 000000000..40a665e7b --- /dev/null +++ b/utils/rector/tests/ChangeFactoryMethodCalls/Fixtures/change-factory-extending-user-defined-factory.php.inc @@ -0,0 +1,53 @@ + +----- + diff --git a/utils/rector/tests/ChangeFactoryMethodCalls/Fixtures/change-legacy-factory-methods-in-object-factory.php.inc b/utils/rector/tests/ChangeFactoryMethodCalls/Fixtures/change-legacy-factory-methods-in-object-factory.php.inc new file mode 100644 index 000000000..c968c8c2a --- /dev/null +++ b/utils/rector/tests/ChangeFactoryMethodCalls/Fixtures/change-legacy-factory-methods-in-object-factory.php.inc @@ -0,0 +1,60 @@ +withoutPersisting() + ->addState([]) + ->withAttributes([]); + } + + protected static function getClass(): string + { + return DummyObject::class; + } +} + +?> +----- +with([]) + ->with([]); + } + + protected static function getClass(): string + { + return DummyObject::class; + } +} + +?> diff --git a/utils/rector/tests/ChangeFactoryMethodCalls/Fixtures/change-legacy-factory-methods-in-other-class.php.inc b/utils/rector/tests/ChangeFactoryMethodCalls/Fixtures/change-legacy-factory-methods-in-other-class.php.inc new file mode 100644 index 000000000..b696bf601 --- /dev/null +++ b/utils/rector/tests/ChangeFactoryMethodCalls/Fixtures/change-legacy-factory-methods-in-other-class.php.inc @@ -0,0 +1,37 @@ +withoutPersisting()->withAttributes([]); + DummyPersistentModelFactory::new()->withoutPersisting()->withAttributes([]); + } +} + +?> +----- +with([]); + DummyPersistentModelFactory::new()->withoutPersisting()->with([]); + } +} + +?> diff --git a/utils/rector/tests/ChangeFactoryMethodCalls/Fixtures/change-legacy-factory-methods-in-proxy-factory.php.inc b/utils/rector/tests/ChangeFactoryMethodCalls/Fixtures/change-legacy-factory-methods-in-proxy-factory.php.inc new file mode 100644 index 000000000..fbe84e9a9 --- /dev/null +++ b/utils/rector/tests/ChangeFactoryMethodCalls/Fixtures/change-legacy-factory-methods-in-proxy-factory.php.inc @@ -0,0 +1,61 @@ +withoutPersisting() + ->addState([]) + ->withAttributes([]); + } + + protected static function getClass(): string + { + return DummyPersistentObject::class; + } +} + +?> +----- +withoutPersisting() + ->with([]) + ->with([]); + } + + protected static function getClass(): string + { + return DummyPersistentObject::class; + } +} + +?> diff --git a/utils/rector/tests/ChangeFactoryMethodCalls/config.php b/utils/rector/tests/ChangeFactoryMethodCalls/config.php new file mode 100644 index 000000000..e54a68c09 --- /dev/null +++ b/utils/rector/tests/ChangeFactoryMethodCalls/config.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Rector\Config\RectorConfig; +use Zenstruck\Foundry\Utils\Rector\ChangeFactoryMethodCalls; + +return static function(RectorConfig $rectorConfig): void { + $rectorConfig->rule(ChangeFactoryMethodCalls::class); +}; diff --git a/utils/rector/tests/ChangeFunctionsCalls/ChangeFunctionsCallsTest.php b/utils/rector/tests/ChangeFunctionsCalls/ChangeFunctionsCallsTest.php new file mode 100644 index 000000000..0ae0214c9 --- /dev/null +++ b/utils/rector/tests/ChangeFunctionsCalls/ChangeFunctionsCallsTest.php @@ -0,0 +1,39 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Utils\Rector\Tests\ChangeFunctionsCalls; + +use Rector\Testing\PHPUnit\AbstractRectorTestCase; + +final class ChangeFunctionsCallsTest extends AbstractRectorTestCase +{ + /** + * @dataProvider provideData + * + * @test + */ + public function test(string $filePath): void + { + $this->doTestFile($filePath); + } + + public static function provideData(): \Iterator + { + return self::yieldFilesFromDirectory(__DIR__.'/Fixtures'); + } + + public function provideConfigFilePath(): string + { + return __DIR__.'/config.php'; + } +} diff --git a/utils/rector/tests/ChangeFunctionsCalls/Fixtures/functions-with-fqcn.php.inc b/utils/rector/tests/ChangeFunctionsCalls/Fixtures/functions-with-fqcn.php.inc new file mode 100644 index 000000000..46e74a638 --- /dev/null +++ b/utils/rector/tests/ChangeFunctionsCalls/Fixtures/functions-with-fqcn.php.inc @@ -0,0 +1,21 @@ + true]); +\Zenstruck\Foundry\repository($someObject); + +\Zenstruck\Foundry\Factory::delayFlush(static fn() => true); +\Zenstruck\Foundry\Test\TestState::configure(faker: null); + +?> +----- + true])); +\Zenstruck\Foundry\Persistence\repository($someObject); + +\Zenstruck\Foundry\Persistence\flush_after(static fn() => true); +\Zenstruck\Foundry\Test\UnitTestConfig::configure(faker: null); + +?> diff --git a/utils/rector/tests/ChangeFunctionsCalls/Fixtures/functions-with-use.php.inc b/utils/rector/tests/ChangeFunctionsCalls/Fixtures/functions-with-use.php.inc new file mode 100644 index 000000000..5315f5c6e --- /dev/null +++ b/utils/rector/tests/ChangeFunctionsCalls/Fixtures/functions-with-use.php.inc @@ -0,0 +1,28 @@ + true]); +repository($someObject); + +Factory::delayFlush(static fn() => true); +TestState::configure(faker: null); + +?> +----- + true])); +\Zenstruck\Foundry\Persistence\repository($someObject); + +\Zenstruck\Foundry\Persistence\flush_after(static fn() => true); +\Zenstruck\Foundry\Test\UnitTestConfig::configure(faker: null); + +?> diff --git a/utils/rector/tests/ChangeFunctionsCalls/config.php b/utils/rector/tests/ChangeFunctionsCalls/config.php new file mode 100644 index 000000000..5a762e585 --- /dev/null +++ b/utils/rector/tests/ChangeFunctionsCalls/config.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Rector\Config\RectorConfig; +use Zenstruck\Foundry\Utils\Rector\ChangeFunctionsCalls; + +return static function(RectorConfig $rectorConfig): void { + $rectorConfig->removeUnusedImports(true); + $rectorConfig->rule(ChangeFunctionsCalls::class); +}; diff --git a/utils/rector/tests/ChangeInstantiatorMethodCalls/ChangeInstantiatorMethodCallsTest.php b/utils/rector/tests/ChangeInstantiatorMethodCalls/ChangeInstantiatorMethodCallsTest.php new file mode 100644 index 000000000..63ba0723d --- /dev/null +++ b/utils/rector/tests/ChangeInstantiatorMethodCalls/ChangeInstantiatorMethodCallsTest.php @@ -0,0 +1,39 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Utils\Rector\Tests\ChangeInstantiatorMethodCalls; + +use Rector\Testing\PHPUnit\AbstractRectorTestCase; + +final class ChangeInstantiatorMethodCallsTest extends AbstractRectorTestCase +{ + /** + * @dataProvider provideData + * + * @test + */ + public function test(string $filePath): void + { + $this->doTestFile($filePath); + } + + public static function provideData(): \Iterator + { + return self::yieldFilesFromDirectory(__DIR__.'/Fixtures'); + } + + public function provideConfigFilePath(): string + { + return __DIR__.'/config.php'; + } +} diff --git a/utils/rector/tests/ChangeInstantiatorMethodCalls/Fixtures/call-in-model-factory.php.inc b/utils/rector/tests/ChangeInstantiatorMethodCalls/Fixtures/call-in-model-factory.php.inc new file mode 100644 index 000000000..008e830a0 --- /dev/null +++ b/utils/rector/tests/ChangeInstantiatorMethodCalls/Fixtures/call-in-model-factory.php.inc @@ -0,0 +1,41 @@ +allowExtraAttributes(['some', 'fields']) + ->alwaysForceProperties(['other', 'fields']) + ->allowExtraAttributes() + ->alwaysForceProperties(); + } +} + +?> +----- +allowExtra(...['some', 'fields']) + ->alwaysForce(...['other', 'fields']) + ->allowExtra() + ->alwaysForce(); + } +} + +?> diff --git a/utils/rector/tests/ChangeInstantiatorMethodCalls/Fixtures/call-in-object-factory-with-private-ctor.php.inc b/utils/rector/tests/ChangeInstantiatorMethodCalls/Fixtures/call-in-object-factory-with-private-ctor.php.inc new file mode 100644 index 000000000..dd87a5f72 --- /dev/null +++ b/utils/rector/tests/ChangeInstantiatorMethodCalls/Fixtures/call-in-object-factory-with-private-ctor.php.inc @@ -0,0 +1,41 @@ +allowExtraAttributes(['some', 'fields']) + ->alwaysForceProperties(['other', 'fields']) + ->allowExtraAttributes() + ->alwaysForceProperties(); + } +} + +?> +----- +allowExtra(...['some', 'fields']) + ->alwaysForce(...['other', 'fields']) + ->allowExtra() + ->alwaysForce(); + } +} + +?> diff --git a/utils/rector/tests/ChangeInstantiatorMethodCalls/Fixtures/call-outside-of-class.php.inc b/utils/rector/tests/ChangeInstantiatorMethodCalls/Fixtures/call-outside-of-class.php.inc new file mode 100644 index 000000000..9922b4cec --- /dev/null +++ b/utils/rector/tests/ChangeInstantiatorMethodCalls/Fixtures/call-outside-of-class.php.inc @@ -0,0 +1,27 @@ +allowExtraAttributes(['some', 'fields']) + ->alwaysForceProperties(['other', 'fields']) + ->allowExtraAttributes() + ->alwaysForceProperties() + +?> +----- +allowExtra(...['some', 'fields']) + ->alwaysForce(...['other', 'fields']) + ->allowExtra() + ->alwaysForce() + +?> diff --git a/utils/rector/tests/ChangeInstantiatorMethodCalls/config.php b/utils/rector/tests/ChangeInstantiatorMethodCalls/config.php new file mode 100644 index 000000000..f1d4b3a35 --- /dev/null +++ b/utils/rector/tests/ChangeInstantiatorMethodCalls/config.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Rector\Config\RectorConfig; +use Zenstruck\Foundry\Utils\Rector\ChangeInstantiatorMethodCalls; + +return static function(RectorConfig $rectorConfig): void { + $rectorConfig->removeUnusedImports(true); + $rectorConfig->rule(ChangeInstantiatorMethodCalls::class); +}; diff --git a/utils/rector/tests/ChangeLegacyClassImports/ChangeLegacyClassImportsTest.php b/utils/rector/tests/ChangeLegacyClassImports/ChangeLegacyClassImportsTest.php new file mode 100644 index 000000000..ebffb8196 --- /dev/null +++ b/utils/rector/tests/ChangeLegacyClassImports/ChangeLegacyClassImportsTest.php @@ -0,0 +1,39 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Utils\Rector\Tests\ChangeLegacyClassUses; + +use Rector\Testing\PHPUnit\AbstractRectorTestCase; + +final class ChangeLegacyClassImportsTest extends AbstractRectorTestCase +{ + /** + * @dataProvider provideData + * + * @test + */ + public function test(string $filePath): void + { + $this->doTestFile($filePath); + } + + public static function provideData(): \Iterator + { + return self::yieldFilesFromDirectory(__DIR__.'/Fixtures'); + } + + public function provideConfigFilePath(): string + { + return __DIR__.'/config.php'; + } +} diff --git a/utils/rector/tests/ChangeLegacyClassImports/Fixtures/change-legacy-classes-import.php.inc b/utils/rector/tests/ChangeLegacyClassImports/Fixtures/change-legacy-classes-import.php.inc new file mode 100644 index 000000000..febcda6dd --- /dev/null +++ b/utils/rector/tests/ChangeLegacyClassImports/Fixtures/change-legacy-classes-import.php.inc @@ -0,0 +1,25 @@ + +----- + diff --git a/utils/rector/tests/ChangeLegacyClassImports/config.php b/utils/rector/tests/ChangeLegacyClassImports/config.php new file mode 100644 index 000000000..e4b35f421 --- /dev/null +++ b/utils/rector/tests/ChangeLegacyClassImports/config.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Rector\Config\RectorConfig; +use Zenstruck\Foundry\Utils\Rector\ChangeLegacyClassImports; + +return static function(RectorConfig $rectorConfig): void { + $rectorConfig->rule(ChangeLegacyClassImports::class); +}; diff --git a/utils/rector/tests/ChangeProxyMethhodCalls/ChangeProxyMethodCallsTest.php b/utils/rector/tests/ChangeProxyMethhodCalls/ChangeProxyMethodCallsTest.php new file mode 100644 index 000000000..24bc74d66 --- /dev/null +++ b/utils/rector/tests/ChangeProxyMethhodCalls/ChangeProxyMethodCallsTest.php @@ -0,0 +1,39 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Utils\Rector\Tests\ChangeLegacyClassUses; + +use Rector\Testing\PHPUnit\AbstractRectorTestCase; + +final class ChangeProxyMethodCallsTest extends AbstractRectorTestCase +{ + /** + * @dataProvider provideData + * + * @test + */ + public function test(string $filePath): void + { + $this->doTestFile($filePath); + } + + public static function provideData(): \Iterator + { + return self::yieldFilesFromDirectory(__DIR__.'/Fixtures'); + } + + public function provideConfigFilePath(): string + { + return __DIR__.'/config.php'; + } +} diff --git a/utils/rector/tests/ChangeProxyMethhodCalls/Fixtures/change-legacy-proxy-methods.php.inc b/utils/rector/tests/ChangeProxyMethhodCalls/Fixtures/change-legacy-proxy-methods.php.inc new file mode 100644 index 000000000..fe9de30bb --- /dev/null +++ b/utils/rector/tests/ChangeProxyMethhodCalls/Fixtures/change-legacy-proxy-methods.php.inc @@ -0,0 +1,67 @@ +object(); + $proxy?->object(); + $proxy->save(); + $proxy->remove(); + $proxy->refresh(); + $proxy->forceSet(); + $proxy->forceGet(); + $proxy->repository(); + $proxy->enableAutoRefresh(); + $proxy->disableAutoRefresh(); + $proxy->withoutAutoRefresh(); +} + +function newProxy(\Zenstruck\Foundry\Persistence\Proxy $proxy): void +{ + $proxy->object(); + $proxy?->object(); + $proxy->save(); + $proxy->remove(); + $proxy->refresh(); + $proxy->forceSet(); + $proxy->forceGet(); + $proxy->repository(); + $proxy->enableAutoRefresh(); + $proxy->disableAutoRefresh(); + $proxy->withoutAutoRefresh(); +} + +?> +----- +_real(); + $proxy?->_real(); + $proxy->_save(); + $proxy->_delete(); + $proxy->_refresh(); + $proxy->_set(); + $proxy->_get(); + $proxy->_repository(); + $proxy->_enableAutoRefresh(); + $proxy->_disableAutoRefresh(); + $proxy->_withoutAutoRefresh(); +} + +function newProxy(\Zenstruck\Foundry\Persistence\Proxy $proxy): void +{ + $proxy->_real(); + $proxy?->_real(); + $proxy->_save(); + $proxy->_delete(); + $proxy->_refresh(); + $proxy->_set(); + $proxy->_get(); + $proxy->_repository(); + $proxy->_enableAutoRefresh(); + $proxy->_disableAutoRefresh(); + $proxy->_withoutAutoRefresh(); +} + +?> diff --git a/utils/rector/tests/ChangeProxyMethhodCalls/config.php b/utils/rector/tests/ChangeProxyMethhodCalls/config.php new file mode 100644 index 000000000..ff56a3f81 --- /dev/null +++ b/utils/rector/tests/ChangeProxyMethhodCalls/config.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Rector\Config\RectorConfig; +use Zenstruck\Foundry\Utils\Rector\ChangeProxyMethodCalls; + +return static function(RectorConfig $rectorConfig): void { + $rectorConfig->rule(ChangeProxyMethodCalls::class); +}; diff --git a/utils/rector/tests/ChangeStaticFactoryFakerCalls/ChangeStaticFactoryFakerCallsTest.php b/utils/rector/tests/ChangeStaticFactoryFakerCalls/ChangeStaticFactoryFakerCallsTest.php new file mode 100644 index 000000000..7da7b183a --- /dev/null +++ b/utils/rector/tests/ChangeStaticFactoryFakerCalls/ChangeStaticFactoryFakerCallsTest.php @@ -0,0 +1,39 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Utils\Rector\Tests\ChangeDisableEnablePersist; + +use Rector\Testing\PHPUnit\AbstractRectorTestCase; + +final class ChangeStaticFactoryFakerCallsTest extends AbstractRectorTestCase +{ + /** + * @dataProvider provideData + * + * @test + */ + public function test(string $filePath): void + { + $this->doTestFile($filePath); + } + + public static function provideData(): \Iterator + { + return self::yieldFilesFromDirectory(__DIR__.'/Fixtures'); + } + + public function provideConfigFilePath(): string + { + return __DIR__.'/config.php'; + } +} diff --git a/utils/rector/tests/ChangeStaticFactoryFakerCalls/Fixtures/change-legacy-function-when-in-kernel-test-case.php.inc b/utils/rector/tests/ChangeStaticFactoryFakerCalls/Fixtures/change-legacy-function-when-in-kernel-test-case.php.inc new file mode 100644 index 000000000..f8fb1057d --- /dev/null +++ b/utils/rector/tests/ChangeStaticFactoryFakerCalls/Fixtures/change-legacy-function-when-in-kernel-test-case.php.inc @@ -0,0 +1,41 @@ + +----- + diff --git a/utils/rector/tests/ChangeStaticFactoryFakerCalls/Fixtures/skip-calls-from-factory.php.inc b/utils/rector/tests/ChangeStaticFactoryFakerCalls/Fixtures/skip-calls-from-factory.php.inc new file mode 100644 index 000000000..f4261fedd --- /dev/null +++ b/utils/rector/tests/ChangeStaticFactoryFakerCalls/Fixtures/skip-calls-from-factory.php.inc @@ -0,0 +1,24 @@ + self::faker(), + 'field2' => Factory::faker() + ]; + } + + protected static function getClass(): string + { + return DummyObject::class; + } +} diff --git a/utils/rector/tests/ChangeStaticFactoryFakerCalls/config.php b/utils/rector/tests/ChangeStaticFactoryFakerCalls/config.php new file mode 100644 index 000000000..410b0f20a --- /dev/null +++ b/utils/rector/tests/ChangeStaticFactoryFakerCalls/config.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Rector\Config\RectorConfig; +use Zenstruck\Foundry\Utils\Rector\ChangeStaticFactoryFakerCalls; + +return static function(RectorConfig $rectorConfig): void { + $rectorConfig->rule(ChangeStaticFactoryFakerCalls::class); +}; diff --git a/utils/rector/tests/Fixtures/DummyKernelTestCase.php b/utils/rector/tests/Fixtures/DummyKernelTestCase.php new file mode 100644 index 000000000..beca52a6c --- /dev/null +++ b/utils/rector/tests/Fixtures/DummyKernelTestCase.php @@ -0,0 +1,26 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Utils\Rector\Tests\Fixtures; + +use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; + +/** + * This is a totally dummy file, which is only helpful for autoloader: + * when this file exist, this class exists, and fixtures files using same class name will be considered as existing + * + * @see tests/Rector/ChangeDisableEnablePersist/Fixtures/change-legacy-function-when-in-kernel-test-case.php.inc + */ +class DummyKernelTestCase extends KernelTestCase +{ +} diff --git a/utils/rector/tests/Fixtures/DummyModelFactoryExtendingUserFactory.php b/utils/rector/tests/Fixtures/DummyModelFactoryExtendingUserFactory.php new file mode 100644 index 000000000..6f712ef64 --- /dev/null +++ b/utils/rector/tests/Fixtures/DummyModelFactoryExtendingUserFactory.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Utils\Rector\Tests\Fixtures; + +final class DummyModelFactoryExtendingUserFactory extends DummyObjectModelFactory +{ + protected function getDefaults(): array + { + return []; + } + + protected static function getClass(): string + { + return DummyObject::class; + } +} diff --git a/utils/rector/tests/Fixtures/DummyObject.php b/utils/rector/tests/Fixtures/DummyObject.php new file mode 100644 index 000000000..abd0bc6ed --- /dev/null +++ b/utils/rector/tests/Fixtures/DummyObject.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Utils\Rector\Tests\Fixtures; + +class DummyObject +{ + public ?int $id = null; + + private function __construct() + { + } + + public static function new(): static + { + return new self(); + } +} diff --git a/utils/rector/tests/Fixtures/DummyObjectFactory.php b/utils/rector/tests/Fixtures/DummyObjectFactory.php new file mode 100644 index 000000000..842907b68 --- /dev/null +++ b/utils/rector/tests/Fixtures/DummyObjectFactory.php @@ -0,0 +1,29 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Utils\Rector\Tests\Fixtures; + +use Zenstruck\Foundry\ObjectFactory; + +final class DummyObjectFactory extends ObjectFactory +{ + public static function class(): string + { + return DummyObject::class; + } + + protected function defaults(): array + { + return []; + } +} diff --git a/utils/rector/tests/Fixtures/DummyObjectModelFactory.php b/utils/rector/tests/Fixtures/DummyObjectModelFactory.php new file mode 100644 index 000000000..b551c898c --- /dev/null +++ b/utils/rector/tests/Fixtures/DummyObjectModelFactory.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Utils\Rector\Tests\Fixtures; + +use Zenstruck\Foundry\ModelFactory; + +/** + * @extends ModelFactory + */ +class DummyObjectModelFactory extends ModelFactory +{ + protected function getDefaults(): array + { + return []; + } + + protected static function getClass(): string + { + return DummyObject::class; + } +} diff --git a/utils/rector/tests/Fixtures/DummyPersistentDocument.php b/utils/rector/tests/Fixtures/DummyPersistentDocument.php new file mode 100644 index 000000000..900497a77 --- /dev/null +++ b/utils/rector/tests/Fixtures/DummyPersistentDocument.php @@ -0,0 +1,23 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Utils\Rector\Tests\Fixtures; + +use Doctrine\ODM\MongoDB\Mapping\Annotations as MongoDB; + +#[MongoDB\Document()] +class DummyPersistentDocument +{ + #[MongoDB\Field(type: 'uuid')] + public ?int $id; +} diff --git a/utils/rector/tests/Fixtures/DummyPersistentDocumentFactory.php b/utils/rector/tests/Fixtures/DummyPersistentDocumentFactory.php new file mode 100644 index 000000000..ec061b8de --- /dev/null +++ b/utils/rector/tests/Fixtures/DummyPersistentDocumentFactory.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Utils\Rector\Tests\Fixtures; + +use Zenstruck\Foundry\ModelFactory; + +/** + * @extends DummyPersistentDocument + */ +final class DummyPersistentDocumentFactory extends ModelFactory +{ + protected function getDefaults(): array + { + return []; + } + + protected static function getClass(): string + { + return DummyPersistentDocument::class; + } +} diff --git a/utils/rector/tests/Fixtures/DummyPersistentModelFactory.php b/utils/rector/tests/Fixtures/DummyPersistentModelFactory.php new file mode 100644 index 000000000..910cd4243 --- /dev/null +++ b/utils/rector/tests/Fixtures/DummyPersistentModelFactory.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Utils\Rector\Tests\Fixtures; + +use Zenstruck\Foundry\ModelFactory; + +/** + * @extends ModelFactory + */ +final class DummyPersistentModelFactory extends ModelFactory +{ + protected function getDefaults(): array + { + return []; + } + + protected static function getClass(): string + { + return DummyPersistentObject::class; + } +} diff --git a/utils/rector/tests/Fixtures/DummyPersistentObject.php b/utils/rector/tests/Fixtures/DummyPersistentObject.php new file mode 100644 index 000000000..cb8dcabea --- /dev/null +++ b/utils/rector/tests/Fixtures/DummyPersistentObject.php @@ -0,0 +1,24 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Utils\Rector\Tests\Fixtures; + +use Doctrine\ORM\Mapping as ORM; + +#[ORM\Entity()] +class DummyPersistentObject +{ + #[ORM\Id] + #[ORM\Column(type: 'uuid')] + public ?int $id; +} diff --git a/utils/rector/tests/Fixtures/DummyTestCase.php b/utils/rector/tests/Fixtures/DummyTestCase.php new file mode 100644 index 000000000..b4d368ff5 --- /dev/null +++ b/utils/rector/tests/Fixtures/DummyTestCase.php @@ -0,0 +1,26 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Utils\Rector\Tests\Fixtures; + +use PHPUnit\Framework\TestCase; + +/** + * This is a totally dummy file, which is only helpful for autoloader: + * when this file exist, this class exists, and fixtures files using same class name will be considered as existing + * + * @see tests/Rector/ChangeDisableEnablePersist/Fixtures/change-legacy-function-when-in-kernel-test-case.php.inc + */ +class DummyTestCase extends TestCase +{ +} diff --git a/utils/rector/tests/RemoveProxyRealObjectMethodCallsForNotProxifiedObjects/Fixtures/remove-object-calls-on-not-persistent-objects.php.inc b/utils/rector/tests/RemoveProxyRealObjectMethodCallsForNotProxifiedObjects/Fixtures/remove-object-calls-on-not-persistent-objects.php.inc new file mode 100644 index 000000000..587ecae91 --- /dev/null +++ b/utils/rector/tests/RemoveProxyRealObjectMethodCallsForNotProxifiedObjects/Fixtures/remove-object-calls-on-not-persistent-objects.php.inc @@ -0,0 +1,41 @@ +_real(); +$proxy->object(); + +/** + * @param Proxy $proxy + */ +function foo(Proxy $proxy): void +{ + $proxy->_real(); + $proxy->object(); +} + +?> +----- + $proxy + */ +function foo(Proxy $proxy): void +{ + $proxy; + $proxy; +} + +?> diff --git a/utils/rector/tests/RemoveProxyRealObjectMethodCallsForNotProxifiedObjects/Fixtures/skip-remove-object-call-after-instantiate.php.inc b/utils/rector/tests/RemoveProxyRealObjectMethodCallsForNotProxifiedObjects/Fixtures/skip-remove-object-call-after-instantiate.php.inc new file mode 100644 index 000000000..3f7742301 --- /dev/null +++ b/utils/rector/tests/RemoveProxyRealObjectMethodCallsForNotProxifiedObjects/Fixtures/skip-remove-object-call-after-instantiate.php.inc @@ -0,0 +1,11 @@ +object(); +instantiate(DummyObject::class, [])->object(); + +?> diff --git a/utils/rector/tests/RemoveProxyRealObjectMethodCallsForNotProxifiedObjects/Fixtures/skip-remove-object-calls-on-persistent-objects.php.inc b/utils/rector/tests/RemoveProxyRealObjectMethodCallsForNotProxifiedObjects/Fixtures/skip-remove-object-calls-on-persistent-objects.php.inc new file mode 100644 index 000000000..6e9d800a7 --- /dev/null +++ b/utils/rector/tests/RemoveProxyRealObjectMethodCallsForNotProxifiedObjects/Fixtures/skip-remove-object-calls-on-persistent-objects.php.inc @@ -0,0 +1,20 @@ +_real(); +$proxy->object(); + +/** + * @param Proxy $proxy + */ +function foo(Proxy $proxy): void +{ + $proxy->_real(); + $proxy->object(); +} + +?> + diff --git a/utils/rector/tests/RemoveProxyRealObjectMethodCallsForNotProxifiedObjects/RemoveProxyRealObjectMethodCallsForNotProxifiedObjectsTest.php b/utils/rector/tests/RemoveProxyRealObjectMethodCallsForNotProxifiedObjects/RemoveProxyRealObjectMethodCallsForNotProxifiedObjectsTest.php new file mode 100644 index 000000000..20d42cea7 --- /dev/null +++ b/utils/rector/tests/RemoveProxyRealObjectMethodCallsForNotProxifiedObjects/RemoveProxyRealObjectMethodCallsForNotProxifiedObjectsTest.php @@ -0,0 +1,39 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Utils\Rector\Tests\ChangeLegacyClassUses; + +use Rector\Testing\PHPUnit\AbstractRectorTestCase; + +final class RemoveProxyRealObjectMethodCallsForNotProxifiedObjectsTest extends AbstractRectorTestCase +{ + /** + * @dataProvider provideData + * + * @test + */ + public function test(string $filePath): void + { + $this->doTestFile($filePath); + } + + public static function provideData(): \Iterator + { + return self::yieldFilesFromDirectory(__DIR__.'/Fixtures'); + } + + public function provideConfigFilePath(): string + { + return __DIR__.'/config.php'; + } +} diff --git a/utils/rector/tests/RemoveProxyRealObjectMethodCallsForNotProxifiedObjects/config.php b/utils/rector/tests/RemoveProxyRealObjectMethodCallsForNotProxifiedObjects/config.php new file mode 100644 index 000000000..5eb0ffe25 --- /dev/null +++ b/utils/rector/tests/RemoveProxyRealObjectMethodCallsForNotProxifiedObjects/config.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Rector\Config\RectorConfig; +use Zenstruck\Foundry\Utils\Rector\RemoveProxyRealObjectMethodCallsForNotProxifiedObjects; + +return static function(RectorConfig $rectorConfig): void { + $rectorConfig->rule(RemoveProxyRealObjectMethodCallsForNotProxifiedObjects::class); +}; diff --git a/utils/rector/tests/RemoveUnproxifyArrayMaps/Fixtures/change-array_map.php.inc b/utils/rector/tests/RemoveUnproxifyArrayMaps/Fixtures/change-array_map.php.inc new file mode 100644 index 000000000..659d66416 --- /dev/null +++ b/utils/rector/tests/RemoveUnproxifyArrayMaps/Fixtures/change-array_map.php.inc @@ -0,0 +1,37 @@ + $proxy->_real(), + [DummyObject::new()] +); +array_map( + fn (Proxy $proxy) => $proxy->object(), + [DummyObject::new()] +); +array_map( + static function (Proxy $proxy) { + return $proxy->object(); + }, + [DummyObject::new()] +); +array_map( + static fn (Proxy $proxy) => $proxy->object(), + DummyObjectModelFactory::createMany(2) +); +?> +----- + diff --git a/utils/rector/tests/RemoveUnproxifyArrayMaps/Fixtures/skip-array_map.php.inc b/utils/rector/tests/RemoveUnproxifyArrayMaps/Fixtures/skip-array_map.php.inc new file mode 100644 index 000000000..99fd543a4 --- /dev/null +++ b/utils/rector/tests/RemoveUnproxifyArrayMaps/Fixtures/skip-array_map.php.inc @@ -0,0 +1,24 @@ + $proxy->_real(), + [DummyObject::new()] +); + +// param is a proxy +array_map( + static fn (Proxy $proxy) => $proxy->object(), + DummyPersistentModelFactory::createMany(2) +); + +// not a object/_real method call +array_map( + static fn (Proxy $proxy) => $proxy->method(), + [DummyObject::new()] +); +?> diff --git a/utils/rector/tests/RemoveUnproxifyArrayMaps/RemoveUnproxifyArrayMapsTest.php b/utils/rector/tests/RemoveUnproxifyArrayMaps/RemoveUnproxifyArrayMapsTest.php new file mode 100644 index 000000000..6a9baccb3 --- /dev/null +++ b/utils/rector/tests/RemoveUnproxifyArrayMaps/RemoveUnproxifyArrayMapsTest.php @@ -0,0 +1,39 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Utils\Rector\Tests\ChangeDisableEnablePersist; + +use Rector\Testing\PHPUnit\AbstractRectorTestCase; + +final class RemoveUnproxifyArrayMapsTest extends AbstractRectorTestCase +{ + /** + * @dataProvider provideData + * + * @test + */ + public function test(string $filePath): void + { + $this->doTestFile($filePath); + } + + public static function provideData(): \Iterator + { + return self::yieldFilesFromDirectory(__DIR__.'/Fixtures'); + } + + public function provideConfigFilePath(): string + { + return __DIR__.'/config.php'; + } +} diff --git a/utils/rector/tests/RemoveUnproxifyArrayMaps/config.php b/utils/rector/tests/RemoveUnproxifyArrayMaps/config.php new file mode 100644 index 000000000..b15cf090a --- /dev/null +++ b/utils/rector/tests/RemoveUnproxifyArrayMaps/config.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Rector\Config\RectorConfig; +use Zenstruck\Foundry\Utils\Rector\RemoveUnproxifyArrayMap; + +return static function(RectorConfig $rectorConfig): void { + $rectorConfig->rule(RemoveUnproxifyArrayMap::class); +};