diff --git a/.env b/.env index da52e373d..673ee0958 100644 --- a/.env +++ b/.env @@ -1,5 +1,12 @@ +# DBMS can be changed by adding the following in .env.local: +#DATABASE_URL="postgresql://zenstruck:zenstruck@127.0.0.1:5433/zenstruck_foundry?serverVersion=15" +#DATABASE_URL="sqlite:///%kernel.project_dir%/var/data.db" DATABASE_URL="mysql://root:1234@127.0.0.1:3307/foundry_test?serverVersion=5.7.42" + +# Mongo ca be disabled with the following in .env.local: +# MONGO_URL="" MONGO_URL="mongodb://127.0.0.1:27018/dbName?compressors=disabled&gssapiServiceName=mongodb" -DATABASE_RESET_MODE="schema" + USE_DAMA_DOCTRINE_TEST_BUNDLE="0" -PHPUNIT_VERSION="9" +USE_FOUNDRY_PHPUNIT_EXTENSION="0" +PHPUNIT_VERSION="9" # allowed values: 9, 10, 11 diff --git a/.gitattributes b/.gitattributes index e3d16b618..00d39e287 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,6 +1,8 @@ * text=auto /.codecov.yml export-ignore +/.editorconfig export-ignore +/.env export-ignore /.gitattributes export-ignore /.github export-ignore /.gitignore export-ignore @@ -12,8 +14,12 @@ /docker-compose.yaml export-ignore /Makefile export-ignore /phpstan.neon export-ignore -/phpunit-dama-doctrine.xml.dist export-ignore +/phpunit export-ignore /phpunit.xml.dist export-ignore +/phpunit-10.xml.dist export-ignore +/phpunit-paratest.xml.dist export-ignore +/psalm.xml export-ignore +/stubs export-ignore /tests export-ignore /utils/rector/tests export-ignore /utils/rector/composer.json export-ignore diff --git a/.github/issue_template.md b/.github/issue_template.md new file mode 100644 index 000000000..5f5d4e031 --- /dev/null +++ b/.github/issue_template.md @@ -0,0 +1,13 @@ + + +| Q | A | +|------------------------------------------------------------------------------------------------------------------------------------|-------------------------| +| Foundry's version? | x.y.z | +| PHP version | 8.1 / 8.2 / 8.3 / 8.4 | +| PHPUnit version | 9 / 10 / 11 / 12 | +| `symfony/framework-bundle` version? | 6.4 / 7.1 / 7.2 | +| Do you use [dama/doctrine-test-bundle](https://github.com/dmaicher/doctrine-test-bundle)? | yes / no | +| Which reset database strategy do you use? | schema / migrate / none | +| Do you use Foundry's [PHPUnit extension](https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#phpunit-extension)? | yes / no | diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0d5bced4d..f8d084949 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,98 +8,55 @@ on: jobs: tests: - name: P:${{ matrix.php }}, S:${{ matrix.symfony }}, D:${{ matrix.database }}, PU:${{ matrix.phpunit }}${{ matrix.deps == 'lowest' && ' (lowest)' || '' }}${{ matrix.use-dama == 1 && contains(matrix.database, 'sql') && ' (dama)' || '' }}${{ !contains(matrix.database, 'sql') && '' || matrix.use-migrate == 1 && ' (migrate)' || ' (schema)' }} + name: P:${{ matrix.php }}, S:${{ matrix.symfony }}, D:${{ matrix.database }}, PU:${{ matrix.phpunit }}${{ matrix.deps == 'lowest' && ' (lowest)' || '' }}${{ matrix.use-phpunit-extension == 1 && ' (phpunit extension)' || '' }} runs-on: ubuntu-latest strategy: fail-fast: false matrix: - php: [ 8.1, 8.2, 8.3 ] - deps: [ highest ] - symfony: [ 6.4.*, 7.0.*, 7.1.* ] - database: [ mysql, mongo ] - use-dama: [ 1 ] - use-migrate: [ 0 ] - phpunit: [ 9 ] + php: [ 8.2, 8.3, 8.4 ] + symfony: [ 6.4.*, 7.1.*, 7.2.* ] + database: [ mysql|mongo ] + phpunit: [ 11 ] + + # default values: + # deps: [ highest ] + # use-phpunit-extension: [ 0 ] + exclude: - - php: 8.1 - symfony: 7.0.* - - php: 8.1 - symfony: 7.1.* + - {php: 8.1, symfony: 7.1.*} + - {php: 8.1, symfony: 7.2.*} include: - - php: 8.3 - deps: highest - symfony: '*' - database: none - use-dama: 1 - use-migrate: 0 - phpunit: 9 - - php: 8.3 - deps: highest - symfony: '*' - database: mysql|mongo - use-dama: 1 - use-migrate: 0 - phpunit: 9 - - php: 8.3 - deps: highest - symfony: '*' - database: pgsql|mongo - use-dama: 1 - use-migrate: 0 - phpunit: 9 - - php: 8.3 - deps: highest - symfony: '*' - database: pgsql - use-dama: 0 - use-migrate: 0 - phpunit: 9 - - php: 8.3 - deps: highest - symfony: '*' - database: sqlite - use-dama: 0 - use-migrate: 0 - phpunit: 9 - - php: 8.3 - deps: lowest - symfony: '*' - database: sqlite - use-dama: 0 - use-migrate: 0 - phpunit: 9 - - php: 8.3 - deps: lowest - symfony: '*' - database: mysql - use-dama: 1 - use-migrate: 0 - phpunit: 9 - - php: 8.3 - deps: highest - symfony: '*' - database: mysql - use-dama: 1 - use-migrate: 1 - phpunit: 9 - - php: 8.3 - deps: highest - symfony: '*' - database: mysql|mongo - use-dama: 1 - use-migrate: 0 - phpunit: 10 - - php: 8.3 - deps: highest - symfony: '*' - database: mysql|mongo - use-dama: 1 - use-migrate: 0 - phpunit: 11 + # php 8.1 + - {php: 8.1, symfony: 6.4.*, phpunit: 9, database: mysql} + + # old PHPUnit versions + - {php: 8.3, symfony: '*', phpunit: 9, database: mysql} + - {php: 8.3, symfony: '*', phpunit: 10, database: mysql} + + # test with no database (PHPUnit 9 is used to prevent some problems with empty data providers) + - {php: 8.3, symfony: '*', phpunit: 9, database: none} + - {php: 8.3, symfony: '*', phpunit: 9, database: none, deps: lowest} + + # One permutation per DBMS + - {php: 8.3, symfony: '*', phpunit: 11, database: mongo} + - {php: 8.3, symfony: '*', phpunit: 11, database: pgsql} + - {php: 8.3, symfony: '*', phpunit: 11, database: sqlite} + - {php: 8.3, symfony: '*', phpunit: 11, database: mysql} + + # lowest deps (one per DBMS) + - {php: 8.3, symfony: '*', phpunit: 9, database: mysql|mongo, deps: lowest} + - {php: 8.3, symfony: '*', phpunit: 9, database: mongo, deps: lowest} + - {php: 8.3, symfony: '*', phpunit: 9, database: pgsql, deps: lowest} + - {php: 8.3, symfony: '*', phpunit: 9, database: sqlite, deps: lowest} + - {php: 8.3, symfony: '*', phpunit: 9, database: mysql, deps: lowest} + + # using Foundry's PHPUnit extension + - {php: 8.3, symfony: '*', phpunit: 11, database: mysql|mongo, use-phpunit-extension: 1} env: DATABASE_URL: ${{ contains(matrix.database, 'mysql') && 'mysql://root:root@localhost:3306/foundry?serverVersion=5.7.42' || contains(matrix.database, 'pgsql') && 'postgresql://root:root@localhost:5432/foundry?serverVersion=15' || contains(matrix.database, 'sqlite') && 'sqlite:///%kernel.project_dir%/var/data.db' || '' }} MONGO_URL: ${{ contains(matrix.database, 'mongo') && 'mongodb://127.0.0.1:27017/dbName?compressors=disabled&gssapiServiceName=mongodb' || '' }} - USE_DAMA_DOCTRINE_TEST_BUNDLE: ${{ matrix.use-dama == 1 && contains(matrix.database, 'sql') && 1 || 0 }} + USE_DAMA_DOCTRINE_TEST_BUNDLE: ${{ contains(matrix.database, 'sql') && 1 || 0 }} + USE_FOUNDRY_PHPUNIT_EXTENSION: ${{ matrix.use-phpunit-extension || 0 }} PHPUNIT_VERSION: ${{ matrix.phpunit }} services: postgres: @@ -145,8 +102,111 @@ jobs: - name: Test run: ./phpunit shell: bash + + test-reset-database: + name: Reset DB - D:${{ matrix.database }} ${{ matrix.use-dama == 1 && ' (dama)' || '' }} ${{ matrix.reset-database-mode == 'migrate' && ' (migrate)' || '' }} ${{ contains(matrix.with-migration-configuration-file, 'transactional') && '(configuration file transactional)' || contains(matrix.with-migration-configuration-file, 'configuration') && '(configuration file)' || '' }}${{ matrix.deps == 'lowest' && ' (lowest)' || '' }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + database: [ mysql, pgsql, sqlite, mysql|mongo ] + use-dama: [ 0, 1 ] + reset-database-mode: [ schema, migrate ] + migration-configuration-file: ['no'] + deps: [ highest, lowest ] + include: + - { database: mongo, migration-configuration-file: 'no', use-dama: 0, reset-database-mode: schema } + - { database: pgsql, migration-configuration-file: 'migration-configuration', use-dama: 0, reset-database-mode: migration } + - { database: pgsql, migration-configuration-file: 'migration-configuration-transactional', use-dama: 0, reset-database-mode: migration } + env: + DATABASE_URL: ${{ contains(matrix.database, 'mysql') && 'mysql://root:root@localhost:3306/foundry?serverVersion=5.7.42' || contains(matrix.database, 'pgsql') && 'postgresql://root:root@localhost:5432/foundry?serverVersion=15' || 'sqlite:///%kernel.project_dir%/var/data.db' }} + MONGO_URL: ${{ contains(matrix.database, 'mongo') && 'mongodb://127.0.0.1:27017/dbName?compressors=disabled&gssapiServiceName=mongodb' || '' }} + USE_DAMA_DOCTRINE_TEST_BUNDLE: ${{ matrix.use-dama == 1 && 1 || 0 }} + DATABASE_RESET_MODE: ${{ matrix.reset-database-mode == 1 && 1 || 0 }} + MIGRATION_CONFIGURATION_FILE: ${{ matrix.migration-configuration-file == 'no' && '' || format('tests/Fixture/MigrationTests/configs/{0}.php', matrix.migration-configuration-file) }} + PHPUNIT_VERSION: 11 + services: + postgres: + image: ${{ contains(matrix.database, 'pgsql') && 'postgres:15' || '' }} env: - DATABASE_RESET_MODE: ${{ matrix.use-migrate == 1 && 'migrate' || 'schema' }} + POSTGRES_USER: root + POSTGRES_PASSWORD: root + POSTGRES_DB: foundry + ports: + - 5432:5432 + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + mongo: + image: ${{ contains(matrix.database, 'mongo') && 'mongo:4' || '' }} + ports: + - 27017:27017 + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: 8.3 + coverage: none + tools: flex + + - name: Install dependencies + uses: ramsey/composer-install@v2 + with: + dependency-versions: ${{ matrix.deps }} + composer-options: --prefer-dist + env: + SYMFONY_REQUIRE: 7.1.* + + - name: Set up MySQL + if: contains(matrix.database, 'mysql') + run: sudo /etc/init.d/mysql start + + - name: Test + run: | + ./phpunit --testsuite reset-database --bootstrap tests/bootstrap-reset-database.php + + # We should be able to run the tests twice in order to check if the second run also starts from a fresh db + # some bugs could be detected this way + ./phpunit --testsuite reset-database --bootstrap tests/bootstrap-reset-database.php + shell: bash + + test-with-paratest: + name: Test with paratest + runs-on: ubuntu-latest + env: + DATABASE_URL: 'mysql://root:root@localhost:3306/foundry?serverVersion=5.7.42' + MONGO_URL: '' + USE_DAMA_DOCTRINE_TEST_BUNDLE: 1 + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: 8.4 + coverage: none + tools: flex + + - name: Install dependencies + uses: ramsey/composer-install@v2 + with: + dependency-versions: highest + composer-options: --prefer-dist + env: + SYMFONY_REQUIRE: 7.2.* + + - name: Set up MySQL + run: sudo /etc/init.d/mysql start + + - name: Test + run: vendor/bin/paratest --processes 1 --configuration phpunit-paratest.xml.dist + shell: bash code-coverage: name: Code Coverage @@ -155,7 +215,8 @@ jobs: DATABASE_URL: postgresql://root:root@localhost:5432/foundry?serverVersion=15 MONGO_URL: mongodb://127.0.0.1:27017/dbName?compressors=disabled&gssapiServiceName=mongodb USE_DAMA_DOCTRINE_TEST_BUNDLE: 1 - PHPUNIT_VERSION: 9 + USE_FOUNDRY_PHPUNIT_EXTENSION: 1 + PHPUNIT_VERSION: 11 services: mongo: image: mongo:4 @@ -236,6 +297,9 @@ jobs: with: composer-options: --prefer-dist + - name: Validate PSR-4 + run: composer dump-autoload --optimize --strict-psr --strict-ambiguous + - name: Install PHPStan run: composer bin phpstan install @@ -256,7 +320,7 @@ jobs: steps: - uses: zenstruck/.github/actions/php-cs-fixer@main with: - php: 8 + php: 8.1 key: ${{ secrets.GPG_PRIVATE_KEY }} token: ${{ secrets.COMPOSER_TOKEN }} diff --git a/.gitignore b/.gitignore index ee9e61fd3..2a766832b 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,7 @@ /.phpunit.cache /vendor/ /bin/tools/*/vendor/ -/bin/tools/php-cs-fixer/composer.lock +/bin/tools/csfixer /build/ /.php-cs-fixer.cache /.phpunit.result.cache @@ -14,5 +14,4 @@ /docker/.makefile /.env.local /docker-compose.override.yaml -/tests/Fixture/Migrations/ /tests/Fixture/Maker/tmp/ diff --git a/CHANGELOG.md b/CHANGELOG.md index e3aa0ddb9..a977db501 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,712 +1,188 @@ # CHANGELOG -## [v2.0.7](https://github.com/zenstruck/foundry/releases/tag/v2.0.7) - -July 12th, 2024 - [v2.0.6...v2.0.7](https://github.com/zenstruck/foundry/compare/v2.0.6...v2.0.7) - -* 5c44991 fix: handle proxies when refreshing entity in Proxy::getState() (#672) by @nikophil -* 49f5e1d Fix faker php urls (#671) by @BackEndTea -* 7719b0d chore(CI): Enable documentation linter (#657) by @cezarpopa - -## [v2.0.2](https://github.com/zenstruck/foundry/releases/tag/v2.0.2) - -June 14th, 2024 - [v2.0.1...v2.0.2](https://github.com/zenstruck/foundry/compare/v2.0.1...v2.0.2) - -* b76c294 fix(2.x): support Symfony 7.1 (#622) by @nikophil -* 9cd97b7 docs: Improve DX for tests (#636) by @matthieumota -* 17b0228 fix(2.x): add back second parameter for after persist callbacks (#631) by @nikophil -* 0c7b3af docs: Fix typo in the upgrade guide (#624) by @stof -* 933ebbd docs: upgrade readme with a link to upgrade guide (#620) by @nikophil - -## [v2.0.1](https://github.com/zenstruck/foundry/releases/tag/v2.0.1) - -June 10th, 2024 - [v2.0.0...v2.0.1](https://github.com/zenstruck/foundry/compare/v2.0.0...v2.0.1) - -* 5f0ce76 Fix `Instantiator::allowExtra` example (#616) by @norkunas -* c2cbcbc fix(orm): reset database instead of dropping the schema when using migrations (#615) by @vincentchalamon - -## [v2.0.0](https://github.com/zenstruck/foundry/releases/tag/v2.0.0) - -June 7th, 2024 - [v1.38.0...v2.0.0](https://github.com/zenstruck/foundry/compare/v1.38.0...v2.0.0) - -* 1393c13 docs: update docs for foundry v2 (#613) by @nikophil -* 0034c78 feat(2.x, make:facotry): add phpdoc conditionnally with --with-phpdoc option (#611) by @nikophil -* a3ec473 fix: prevent refresh error with autorefresh (#610) by @nikophil -* ca8258a minor(make:factory): default false for ODM mapping (#605) by @nikophil -* df5bb6b minor(2.x): remove APP_ENV=test from phpunit.xml (#603) by @nikophil -* 0479ddf fix(2.x): allow sequence with associative arrays (#595) by @nikophil -* d1509fb fix(2.x): support readonly entities (#599) by @nikophil -* 2c8e048 feat(2.x): allow to configure default namespace fo make:factory (#600) by @nikophil -* 9174dc6 fix: restore PHPUnit error handler (#587) by @nikophil -* 4156302 tests: asserts story works without persistence (#589) by @nikophil -* ec2c895 minor: add phpunit attributes (#576) by @nikophil -* 90cc839 feat(sequence): sequence attributes should be compatible with 1.x (#575) by @nikophil -* 60ec275 fix: sqlite with orm v2 (#574) by @kbond -* ae82186 feat: compatibility with ORM v3 (#572) by @nikophil, @kbond -* 624e8d2 feat: foundry 2.0 🎉 by @nikophil -* e74f6b9 fix(rector) second argument for many() is optional (#515) by @nikophil -* a555474 fix(rector): repository method is static (#515) by @nikophil -* 53f25a2 rector: rewrite phpdoc (#571) by @nikophil -* ecbc615 refactor: Foundry 2 BC layer (#515) by @nikophil - -## [v1.38.0](https://github.com/zenstruck/foundry/releases/tag/v1.38.0) - -June 7th, 2024 - [v1.37.0...v1.38.0](https://github.com/zenstruck/foundry/compare/v1.37.0...v1.38.0) - -* b3adc86 docs(v1): improve upgrade guide (#614) by @nikophil -* 691741f doc: update (#594) by @kbond, @nikophil -* 431afa3 minor(make:factory): default false for `embedded` and `targetDocument` mappings (#602) by @melkamar -* 6102488 fix(1.x): support entities with readonly properties (#598) by @nikophil -* 5af3f0f feat: Configure default_namespace for make_story (#592) by @dmitryuk, a.dmitryuk -* 00e6b86 fix(rector): misc fixes in rector rules (#586) by @nikophil -* afad017 fix: can use ObjectFactories as service (#585) by @nikophil -* b66ed4c fix: restore factory collection type (#584) by @nikophil -* 7cfab99 fix: restore error handler in Foundry's traits (#577) by @nikophil -* d55c728 fix(sca): fix type system with legacy FactoryCollection (#583) by @nikophil -* d38568f docs: typo fixes (#578) by @jrushlow -* bbcef6a refactor: add a BC layer for 2.x (#515) by @nikophil - -## [v1.37.0](https://github.com/zenstruck/foundry/releases/tag/v1.37.0) - -March 20th, 2024 - [v1.36.1...v1.37.0](https://github.com/zenstruck/foundry/compare/v1.36.1...v1.37.0) - -* 4a9b00e feat: support `doctrine/orm` 3 (#569) by @nikophil -* 68148e1 use phpunit attributes (#562) by @jrushlow -* f803868 fix: only run maker test on php > 8.0 (#570) by @nikophil -* 669bc2d chore: drop SF 6.3 from matrix (#558) by @kbond -* cf87b97 minor: add sqlite to CI (#557) by @kbond - -## [v1.36.1](https://github.com/zenstruck/foundry/releases/tag/v1.36.1) - -December 14th, 2023 - [v1.36.0...v1.36.1](https://github.com/zenstruck/foundry/compare/v1.36.0...v1.36.1) - -* 8ea43d5 minor: add Symfony 7.0 to the test matrix (#539) by @kbond -* 4c4d099 minor: update psalm (#539) by @kbond -* c9ddf74 minor: update/fix phpstan issues (#539) by @kbond -* de3b26e minor: allow `dama/doctrine-test-bundle` 8.x (#539) by @kbond -* a1a22ce doc: fix memoize example (#526) by @kbond -* b7c0cb7 minor(ci): remove mysql/no-dama from matrix (#521) by @kbond -* e41bbe8 doc: add warning about complex global state performance (#520) by @kbond -* 81eb3b8 doc: add section on `paratestphp/paratest` (#520) by @kbond -* 8666777 doc: adjust test performance section (#520) by @kbond -* a0bbf5b fix: phpunit xsd path (#520) by @kbond -* ed0cd7b chore: remove Makefile (#513) by @nikophil - -## [v1.36.0](https://github.com/zenstruck/foundry/releases/tag/v1.36.0) - -October 13th, 2023 - [v1.35.0...v1.36.0](https://github.com/zenstruck/foundry/compare/v1.35.0...v1.36.0) - -* 5ffa1cd minor: allow Symfony 7.0 (#509) by @kbond -* 925eab3 fix: proxy should not try to refresh when persist is disabled (#508) by @nikophil -* e8b3ff5 feat: allow stories to call their own pool (#506) by @nikophil -* 881280e docs: Move note about immutability into States section (#504) by @pavelmaca -* d6eb810 doc: fix typo (#502) by @ternel -* 131517a minor: flag repo as dev-dependency for composer (#501) by @Chris53897, @Chris8934 -* 4da5628 fix: replace doctrine:query:sql with dbal:run-sql (#492) by @KDederichs - -## [v1.35.0](https://github.com/zenstruck/foundry/releases/tag/v1.35.0) - -August 10th, 2023 - [v1.34.1...v1.35.0](https://github.com/zenstruck/foundry/compare/v1.34.1...v1.35.0) - -* 342a2c9 feat: disable persist globally (#488) by @nikophil -* fbccdd3 fix: do not flush global state twice when dama is not enabled (#489) by @nikophil - -## [v1.34.1](https://github.com/zenstruck/foundry/releases/tag/v1.34.1) - -August 4th, 2023 - [v1.34.0...v1.34.1](https://github.com/zenstruck/foundry/compare/v1.34.0...v1.34.1) - -* 2433482 minor: set `Factory::isPersisting()` protected (#486) by @nikophil -* b86ca0a fix: deprecated code in `StubCommand` (#485) by @kbond - -## [v1.34.0](https://github.com/zenstruck/foundry/releases/tag/v1.34.0) - -July 12th, 2023 - [v1.33.0...v1.34.0](https://github.com/zenstruck/foundry/compare/v1.33.0...v1.34.0) - -* 850858e feat(lazy): Add memoized LazyValue (#475) by @ndench, @kbond -* 6d2c7ce fix: maker with cs fixer (#476) by @nikophil -* b6b6501 Fix a missing parenthesis in docs (#470) by @jmsche - -## [v1.33.0](https://github.com/zenstruck/foundry/releases/tag/v1.33.0) - -May 23rd, 2023 - [v1.32.0...v1.33.0](https://github.com/zenstruck/foundry/compare/v1.32.0...v1.33.0) - -* 98eca98 tests: can create object with fields name different from construct (#466) by @nikophil -* b48399d feat: drop all connections before dropping db for postgres (#458) by @nikophil -* c2719d1 dependencies: bump faker to 1.10 (#464) by @nikophil -* a5ed31c feat: allow setters as factory attribute (#457) by @nikophil - -## [v1.32.0](https://github.com/zenstruck/foundry/releases/tag/v1.32.0) - -May 11th, 2023 - [v1.31.0...v1.32.0](https://github.com/zenstruck/foundry/compare/v1.31.0...v1.32.0) - -* 27b4d7f feat: allow ModelFactory::findOrCreate() in unit tests (#461) by @nikophil -* 68c6a5e ci: add Symfony 6.3 to test matrix (#459) by @kbond -* 912f134 fix: makefile minor fixes (#451) by @nikophil -* d8fafed minor: improve makefile (#449) by @nikophil -* 9cce6dc docs: add static ide hint to stories (#448) by @adrianrudnik - -## [v1.31.0](https://github.com/zenstruck/foundry/releases/tag/v1.31.0) - -March 27th, 2023 - [v1.30.3...v1.31.0](https://github.com/zenstruck/foundry/compare/v1.30.3...v1.31.0) - -* ac35acf fix: nested factories not persisting should not throw error (#444) by @nikophil -* d346c68 feat: Have delayFlush return the callback's return (#442) by @HypeMC - -## [v1.30.3](https://github.com/zenstruck/foundry/releases/tag/v1.30.3) - -March 22nd, 2023 - [v1.30.2...v1.30.3](https://github.com/zenstruck/foundry/compare/v1.30.2...v1.30.3) - -* c31d653 fix(RepositoryProxy::find()): allow not entity object in (#441) by @nikophil - -## [v1.30.2](https://github.com/zenstruck/foundry/releases/tag/v1.30.2) - -March 22nd, 2023 - [v1.30.1...v1.30.2](https://github.com/zenstruck/foundry/compare/v1.30.1...v1.30.2) - -* 2f71013 fix: allow any object passed as find() criteria (#440) by @nikophil - -## [v1.30.1](https://github.com/zenstruck/foundry/releases/tag/v1.30.1) - -March 21st, 2023 - [v1.30.0...v1.30.1](https://github.com/zenstruck/foundry/compare/v1.30.0...v1.30.1) - -* fba0933 fix: regression from embedded object in findOrCreate function (#438) by @nicolasne, Nicolas Nénon, @kbond - -## [v1.30.0](https://github.com/zenstruck/foundry/releases/tag/v1.30.0) - -March 20th, 2023 - [v1.29.0...v1.30.0](https://github.com/zenstruck/foundry/compare/v1.29.0...v1.30.0) - -* c5ca551 refactor: deprecate AnonymousFactory class and factory() helper (#436) by @nikophil -* 837dbf6 fix(global state): register story as service in StoryManager (#434) by @nikophil -* 230f75e feat: findOrCreate() can use embeddables (#432) by @nikophil -* cacf9ea minor: add conflict for `doctrine/mongodb-odm` for false deprecation (#435) by @kbond -* 6cc9541 fix: running `array_values()` on array attribute (#435) by @kbond -* 083a9b9 bug: add failing test for attribute that is array (#435) by @kbond -* 0094f09 refactor: simplify atrtibutes normalization (#423) by @nikophil -* d8a94ce doc: Add introduction to `index.rst` for symfony.com accessibility (#429) by @GromNaN - -## [v1.29.0](https://github.com/zenstruck/foundry/releases/tag/v1.29.0) - -February 28th, 2023 - [v1.28.0...v1.29.0](https://github.com/zenstruck/foundry/compare/v1.28.0...v1.29.0) - -* 041b597 feat: add `LazyValue` to calculate attribute values only when needed (#427) by @kbond, @mpdude -* 5f4f679 feat(bundle): allow registering custom Faker Providers (#425) by @kbond -* b0bd5cb fix: restore Factory::$cascadePersist (#424) by @nikophil -* f8db2af minor: remove `Factory::$cascadePersist` (#422) by @nikophil -* edcd083 fix(doc): return type syntax (#420) by @benblub, @OskarStark -* bfb1134 feat: validate generate factory with static analysis (#419) by @nikophil -* a7502e7 docs: show how to document magic methods in stories (#418) by @nikophil -* bbc3b62 fix(ci): don't run fixcs/sync-with-template on forks (#417) by @kbond -* c409dc7 minor(ci): drop testing unsupported Symfony versions (6.0/6.1) (#417) by @kbond -* 626acba fix: foundry should work witout maker bundle (#416) by @nikophil -* 0ff998a chore: migrate phpunit config-file (dama) for phpunit 9 format (#413) by @Chris53897, @Chris8934 -* 30e0bdb feat(make:factory): match directory/namespace structure (#411) by @nikophil -* a6d5ead docs: Replace annotations with attributes in docs (#412) by @ker0x - -## [v1.28.0](https://github.com/zenstruck/foundry/releases/tag/v1.28.0) - -January 24th, 2023 - [v1.27.0...v1.28.0](https://github.com/zenstruck/foundry/compare/v1.27.0...v1.28.0) - -* e54c757 fix: remove type in closure in RepositoryProxy::proxyResult() (#410) by @nikophil -* 6f54bbb chore: easy way to require lowest deps (#407) by @nikophil -* 0a4b4ec feat: drop support of symfony 4, remove deprecation for console/command (#398) by @Chris53897, @Chris8934 -* 467f62f feat(make:factory): handle name collision (#402) by @nikophil -* 1c96c86 chore: migrate phpunit config-file for phpunit 9 format (#404) by @Chris53897, @Chris8934 -* aac5aba docs: fix spelling of annotations in CHANGELOG.md (#403) by @Chris53897, @Chris8934 -* 2875dd7 feat(make:factory): list embeddables (#400) by @nikophil -* 3a40f1a minor: meaningful error for faker in data provider (#399) by @nikophil -* 48398f1 feat(make:factory): default value for enum types (#393) by @nikophil -* 6cf06c4 tests: reactivate self deprecations (#392) by @nikophil - -## [v1.27.0](https://github.com/zenstruck/foundry/releases/tag/v1.27.0) - -January 9th, 2023 - [v1.26.0...v1.27.0](https://github.com/zenstruck/foundry/compare/v1.26.0...v1.27.0) - -* 7b97ac2 feat: add $criteria param to RepositoryAssertions::empty() (#391) by @nikophil - -## [v1.26.0](https://github.com/zenstruck/foundry/releases/tag/v1.26.0) - -December 29th, 2022 - [v1.25.0...v1.26.0](https://github.com/zenstruck/foundry/compare/v1.25.0...v1.26.0) - -* 79913c3 feat: create parameter to RepositoryAssertions::count() methods (#390) by @nikophil -* 4df0f40 chore: improve makefile (#382) by @nikophil -* 49da6a0 feat(make:factory): use autocompletion for no persistence classes (#383) by @nikophil - -## [v1.25.0](https://github.com/zenstruck/foundry/releases/tag/v1.25.0) - -December 22nd, 2022 - [v1.24.1...v1.25.0](https://github.com/zenstruck/foundry/compare/v1.24.1...v1.25.0) +## [v2.3.4](https://github.com/zenstruck/foundry/releases/tag/v2.3.4) + +February 14th, 2025 - [v2.3.3...v2.3.4](https://github.com/zenstruck/foundry/compare/v2.3.3...v2.3.4) + +* ad8d72c fix: can index one to many relationships based on "indexBy" (#815) by @nikophil + +## [v2.3.2](https://github.com/zenstruck/foundry/releases/tag/v2.3.2) + +February 1st, 2025 - [v2.3.1...v2.3.2](https://github.com/zenstruck/foundry/compare/v2.3.1...v2.3.2) + +* 46464cc chore(ci): misc improvments in CI permutations (#797) by @nikophil +* 86c5aab test: assert updates are implicitly persisted (#781) by @nikophil +* 54c7424 feat: deprecate when Factories trait is not used in a KernelTestCase (#766) by @nikophil +* 9937b11 chore: add issue template (#795) by @nikophil +* 884113f fix: simplify reset database extension (#779) by @nikophil +* bd50f41 fix: add unpersisted object to relation (#780) by @nikophil +* 17388bc tests: transform "migrate" testsuite into "reset database" testsuite (#763) by @nikophil +* e45913e fix: propagate "schedule for insert" to factory collection (#775) by @nikophil +* d9262cc fix: fix .gitattributes and `#[RequiresPhpUnit]` versions (#792) by @nikophil +* 57c42bc tests: fix a test after a bug was resolved in doctrine migrations (#791) by @nikophil +* 200cfdd [Doc] Fix misc issues (#789) by @javiereguiluz +* 553807b minor: add platform config to mysql docker container (#788) by @kbond +* 316d3c7 doc: fix typo (#782) by @norival +* 0d66c02 minor: use refresh for detached entities (#778) by @nikophil +* 29b48a1 test: add orphan removal premutation (#777) by @nikophil +* c00b3f1 fix: isPersisted must work when id is known in advance (#774) by @nikophil +* f303f3f fix: remove _refresh call from create object process (#773) by @nikophil +* 65cedbf fix: use a "placeholder" for inversed one-to-one (#755) by @nikophil +* 5f99506 minor: introduce PerssitenceManager::isPersisted() (#754) by @nikophil +* 9948d6a fix(ci): change PHP version used by PHP CS-Fixer (#768) by @nikophil +* cf3cc8b docs: Minor syntax fix (#767) by @javiereguiluz +* e8f9a92 docs: clarify default attributes and fixed some syntax issues (#765) by @nikophil, @javiereguiluz +* 1db5ced tests: validate PSR-4 in CI (#762) by @nikophil +* cafc693 [Docs fix] Just spelling in docs (#761) by @GrinWay +* d192c4a [Docs fix] Proxy::_save() instead of Proxy::save() (#760) by @GrinWay +* ff7210a [Docs fix] Factory::_real() instead Factory::object() (#759) by @GrinWay +* d1240b1 fix: RequiresPhpunit should use semver constraint by @nikophil +* fd2e38c chore: upgrade to phpstan 2 (#748) by @nikophil +* 23b4ec4 tests: automatically create cascade persist permutations (#666) by @nikophil +* f4ba5d8 tests: add CI permutation with windows (#747) by @nikophil +* c17ef91 fix: define FactoryCollection type more precisely (#744) by @nikophil +* 98f018c feat: schedule objects for insert right after instantiation (#742) by @nikophil +* 2dcad10 feat: provide current factory to hook (#738) by @nikophil +* ea89504 fix: pass to `afterPersist` hook the attributes from `beforeInstantiate` (#745) by @nikophil, @kbond -* b101604 fix: ci by @kbond -* cc22eac chore: fix cs (#386) by @kbond -* e0944bb chore(ci): sync meta files and automate cs fixer (#386) by @kbond -* d970d7a minor: Reference non deprecated method (#387) by @jongotlin -* 7ab0740 minor(story): make `Story::getState()` protected (#385) by @kbond -* 9a6f28e chore: availability to chose php version (#376) (#381) by @nikophil -* aaeb6cf doc: display downloads badge (#380) by @kbond -* e4d5fcb chore(ci): test on PHP 8.2 (#361) by @kbond -* 555d547 chore: change test context with .env (#375) by @nikophil -* 2eb52ed feat(make:factory): auto create missing factories (#372) by @nikophil -* 6bc81b1 refactor: set all fixtures class name unique (#374) by @nikophil -* 892ed14 feat(make:factory): improve Doctrine default fields guesser (#364) by @nikophil -* 7b08360 doc: fix header (#370) by @seb-jean +## [v2.3.1](https://github.com/zenstruck/foundry/releases/tag/v2.3.1) -## [v1.24.1](https://github.com/zenstruck/foundry/releases/tag/v1.24.1) +December 12th, 2024 - [v2.3.0...v2.3.1](https://github.com/zenstruck/foundry/compare/v2.3.0...v2.3.1) -November 29th, 2022 - [v1.24.0...v1.24.1](https://github.com/zenstruck/foundry/compare/v1.24.0...v1.24.1) +* 138801d chore: remove error handler hack (#729) by @nikophil +* cd9dbf5 refactor: extract reset:migration tests in another testsuite (#692) by @nikophil -* 6588804 dependencies: allow symfony/string 5.4 (#369) by @HypeMC -* 9e5450e docs: fix namespaces in global_state example (#366) by @OskarStark -* 917aba5 docs: fix onfig value (#368) by @OskarStark -* 7773f7e docs: fix config key (#367) by @OskarStark +## [v2.3.0](https://github.com/zenstruck/foundry/releases/tag/v2.3.0) -## [v1.24.0](https://github.com/zenstruck/foundry/releases/tag/v1.24.0) +December 11th, 2024 - [v2.2.2...v2.3.0](https://github.com/zenstruck/foundry/compare/v2.2.2...v2.3.0) -November 25th, 2022 - [v1.23.0...v1.24.0](https://github.com/zenstruck/foundry/compare/v1.23.0...v1.24.0) +* b16b227 Update index.rst (#740) by @OskarStark, @nikophil +* 854220f Figo highlighting and use CPP (#740) by @OskarStark +* dfe6bab tests: add paratest permutation (#736) by @nikophil +* af64c35 fix: detect if relation is oneToOne (#732) by @nikophil +* 59867c3 minor: change versions requirements (#737) by @nikophil +* c8f5046 Fix PHPUnit constraint requirement in FoundryExtension (#735) by @HypeMC +* 4cb7447 Typo in Immutable section (#731) by @franckranaivo +* 403d9e9 fix: Fix the parameter name of the first and last methods (#730) by @marien-probesys +* 0867ad6 feat: add `#[WithStory]` attribute (#728) by @nikophil +* c5d0bdd fix: can create inversed one to one with non nullable (#726) by @nikophil +* 0e7ac6f docs: Fix Story phpdocs (#727) by @simondaigre, @nikophil +* f48ffd1 fix: can create inversed one to one (#659) by @nikophil +* 6d08784 fix: bug with one to many (#722) by @nikophil +* efadea8 docs:fix code blocks not showing up (#723) by @AndreasA +* edf287e minor: Add templated types to flush_after (#719) by @BackEndTea -* f5e9eae minor: use --no-persistence instead of --not-persisted (#365) by @nikophil -* 730c0d9 chore: rename service ids (#363) by @kbond -* 19acc72 feat: add `RepositoryProxy::inner()` (#362) by @kbond -* a003bac refactor(make:factory): split command with DefaultPropertiesGuesser (#357) by @nikophil -* d8eca88 chore(ci): test on Symfony 6.2 (#359) by @kbond -* 20ac349 refactor(make:factory): use value object to render template (#354) by @nikophil -* 4e5f9d9 feat(make:factory): use factories to default non-nullable relationships (#351) by @nikophil, @benblub -* 8332956 feat: make `Story::get()` static (implies `Story::load()->get()`) (#253) by @kbond -* b89bcff minor(make:factory): misc enhancements of maker (#345) by @nikophil -* 3cc95a5 minor: remove php 7.4 related tests (#349) by @nikophil -* 8a055b0 chore: fix docker cache (#350) by @nikophil -* 18ea4fb feat(make:factory): create factory for not-persisted objects (#343) by @nikophil -* 64786fc fix: typo in docs (#348) by @nikophil -* 1a98fc4 chore: Use composer 2.4 (#346) by @OskarStark -* 96c4cbe minor(make:factory): Use `@see`/`@todo` annotations (#344) by @OskarStark -* cbeb2ce fix: adjust docblocks to remove PhpStorm errors (#341) by @kbond -* cd1e394 fix: use orm limit length in factory (#294) by @MrYamous -* b1d7ce3 [feature] add default for Mongo properties in (#340) by @nikophil -* 778607a [chore] use cache for docker CI (#339) by @nikophil -* c662eb3 [feature] auto add phpstan annotations in make:factory (#338) by @nikophil -* 2bd046f [docs] sort phpstan-method annotations (#333) by @OskarStark -* 0460741 [docs] remove obsolete section (#335) by @nikophil -* 2c34baf [bug] Typos in Makefile (#330) by @OskarStark -* 65924b2 [bug] typos in docs (#331) by @OskarStark -* 6b48878 [chore] upgrade ci actions (#329) by @kbond -* 210faff [chore] use phpstan instead of psalm (#328) by @nikophil -* 51f1bc0 [refactor] modernize code with rector (#327) by @nikophil -* 8423b75 [chore] adjust `.symfony.bundle.yaml` for new branch (#325) by @kbond -* 5a05513 [feature] require php8+ (#327) by @kbond +## [v2.2.2](https://github.com/zenstruck/foundry/releases/tag/v2.2.2) -## [v1.23.0](https://github.com/zenstruck/foundry/releases/tag/v1.23.0) +November 5th, 2024 - [v2.2.1...v2.2.2](https://github.com/zenstruck/foundry/compare/v2.2.1...v2.2.2) -November 10th, 2022 - [v1.22.1...v1.23.0](https://github.com/zenstruck/foundry/compare/v1.22.1...v1.23.0) +* 3282f24 Remove @internal from db resetter interfaces (#715) by @HypeMC +* 870cb42 docs: fix missing comma in upgrade doc (#718) by @justpilot -* f43b067 [chore] clean up CI (#324) by @nikophil -* 3588274 [feature] Allow to use foundry without Doctrine (#323) by @nikophil -* 7598467 [feature] [remove bundleless usge] configure global state with config (#322) by @nikophil -* e417945 [feature] [remove bundleless usge] use config instead of environment variables (#320) by @nikophil -* cada0cf [feature] pass an index to `FactoryCollection` attributes (#318) by @nikophil -* d120b1c [minor] fix `bamarni/composer-bin-plugin` deprecations (#313) by @kbond -* a3eefc1 [minor] remove branch alias (#313) by @kbond -* cf7d75e [minor] remove unneeded bin script (#310) by @kbond -* cd42774 [feature] add make migrations (#309) by @nikophil -* cb9a4ec [feature] add a docker stack (#306) by @nikophil +## [v2.2.1](https://github.com/zenstruck/foundry/releases/tag/v2.2.1) -## [v1.22.1](https://github.com/zenstruck/foundry/releases/tag/v1.22.1) +October 31st, 2024 - [v2.2.0...v2.2.1](https://github.com/zenstruck/foundry/compare/v2.2.0...v2.2.1) -September 28th, 2022 - [v1.22.0...v1.22.1](https://github.com/zenstruck/foundry/compare/v1.22.0...v1.22.1) +* 496a7a8 fix: Change `RepositoryDecorator::inner()` visibility to public (#714) by @marienfressinaud +* dfeb247 chore: test Foundry on PHP 8.4 & sf 7.2 (#709) by @nikophil +* 2b12ef0 chore: simplify CI matrix (#708) by @nikophil -* 8d41ca8 [bug] discover relations with inheritance (#300) by @NorthBlue333 -* ae6bda2 [bug] multiple relationships with same entity (#302) by @NorthBlue333 +## [v2.2.0](https://github.com/zenstruck/foundry/releases/tag/v2.2.0) -## [v1.22.0](https://github.com/zenstruck/foundry/releases/tag/v1.22.0) +October 24th, 2024 - [v2.1.0...v2.2.0](https://github.com/zenstruck/foundry/compare/v2.1.0...v2.2.0) -September 21st, 2022 - [v1.21.1...v1.22.0](https://github.com/zenstruck/foundry/compare/v1.21.1...v1.22.0) +* a549c10 docs: using factories in data providers (#707) by @nikophil +* 470d927 docs: how to extend database reset mechanism (#706) by @nikophil +* 2014ed9 feature: allow to use `Factory::create()` and factory service in data providers (#648) by @nikophil +* df568da refactor: make "database reset" mechanism extendable (#690) by @nikophil +* 4fb0b25 docs: add missing docs (#703) by @nikophil +* fa1d527 minor: misc fixes for sca (#705) by @nikophil +* 0d570cc refactor: fix proxy system and introduce psalm extension (#704) by @nikophil -* 4fb5fb8 [feature] Introduce Sequences (#298) by @nikophil +## [v2.1.0](https://github.com/zenstruck/foundry/releases/tag/v2.1.0) -## [v1.21.1](https://github.com/zenstruck/foundry/releases/tag/v1.21.1) +October 3rd, 2024 - [v2.0.9...v2.1.0](https://github.com/zenstruck/foundry/compare/v2.0.9...v2.1.0) + +* 0f72ea5 fix: allow non object state in stories (#699) by @Brewal +* 6482357 feat: allow to configure migrations configuration files (#686) by @MatTheCat + +## [v2.0.9](https://github.com/zenstruck/foundry/releases/tag/v2.0.9) + +September 2nd, 2024 - [v2.0.8...v2.0.9](https://github.com/zenstruck/foundry/compare/v2.0.8...v2.0.9) + +* b0a5d3d Fix Psalm TooManyTemplateParams (#693) by @ddeboer + +## [v2.0.8](https://github.com/zenstruck/foundry/releases/tag/v2.0.8) + +August 29th, 2024 - [v2.0.7...v2.0.8](https://github.com/zenstruck/foundry/compare/v2.0.7...v2.0.8) + +* 3eebbf9 Have `flush_after()` return the callback's return (#691) by @HypeMC +* 33d5870 doc: Fix range call instead of many (#688) by @ternel +* 33595b9 chore: add a wrapper for PHPUnit binary (#683) by @nikophil +* 8bf8c4c docs: Fix CategoryStory codeblock (#681) by @smnandre +* f89d43e doc: Minor fixes (#679) by @smnandre +* 65c1cc2 fix: add phpdoc to improve proxy factories autocompletion (#675) by @nikophil -September 12th, 2022 - [v1.21.0...v1.21.1](https://github.com/zenstruck/foundry/compare/v1.21.0...v1.21.1) - -* 3b105a7 [bug] Fix usage of faker dateTime in factory maker (#297) by @jmsche -* 0663f29 [doc] Fix code block docs about faker seed (#296) by @jmsche -* b57d067 [doc] fix typo (#295) by @Chris53897 -* 4577ef4 [minor] Improve deprecation message for `createMany()` (#291) by @gazzatav, @kbond - -## [v1.21.0](https://github.com/zenstruck/foundry/releases/tag/v1.21.0) - -June 27th, 2022 - [v1.20.0...v1.21.0](https://github.com/zenstruck/foundry/compare/v1.20.0...v1.21.0) - -* e02fbe1 [doc] update config for 5.4+ (#285) by @kbond -* 39258de [feature] add configuration option for faker generator seed (#285) by @kbond -* 1bd05ce [feature] re-save created object after "afterPersist" events called (#279) by @kbond -* 195c815 [bug] Use DateTimeImmutable with immutable ORM types (#283) by @HypeMC - -## [v1.20.0](https://github.com/zenstruck/foundry/releases/tag/v1.20.0) - -June 20th, 2022 - [v1.19.0...v1.20.0](https://github.com/zenstruck/foundry/compare/v1.19.0...v1.20.0) - -* 6009499 [feature] add `Story::getPool()` (#282) by @kbond - -## [v1.19.0](https://github.com/zenstruck/foundry/releases/tag/v1.19.0) - -May 24th, 2022 - [v1.18.2...v1.19.0](https://github.com/zenstruck/foundry/compare/v1.18.2...v1.19.0) - -* 46de01a [feature] Handle variadic constructor arguments (#277) by @ndench -* f5d9177 [minor] use symfony/phpunit-bridge 6+ by @kbond -* 09b0ae2 [minor] fix sca by @kbond - -## [v1.18.2](https://github.com/zenstruck/foundry/releases/tag/v1.18.2) - -April 29th, 2022 - [v1.18.1...v1.18.2](https://github.com/zenstruck/foundry/compare/v1.18.1...v1.18.2) - -* 2b2d2e7 [minor] allow `doctrine/persistence` 3 (#275) by @kbond -* 429466e [doc] add note about phpstan docblocks (#274) by @kbond, Jacob Dreesen - -## [v1.18.1](https://github.com/zenstruck/foundry/releases/tag/v1.18.1) - -April 22nd, 2022 - [v1.18.0...v1.18.1](https://github.com/zenstruck/foundry/compare/v1.18.0...v1.18.1) - -* ff9e4ef [bug] fix embeddable support when used with file (ie xml) mapping (#271) by @kbond -* 40a5a1e [minor] support Symfony 6.1 (#267) by @kbond - -## [v1.18.0](https://github.com/zenstruck/foundry/releases/tag/v1.18.0) - -April 11th, 2022 - [v1.17.0...v1.18.0](https://github.com/zenstruck/foundry/compare/v1.17.0...v1.18.0) - -* b9d2ed3 [feature] add `Factory::delayFlush()` (#84) by @kbond -* 91609b4 [minor] remove scrutinizer (#266) by @kbond -* 8117f40 [minor] allow `dama/doctrine-test-bundle` 7.0 (#266) by @kbond -* 6052e81 [minor] Revert "[bug] fix global state with symfony/framework-bundle >= 5.4.6/6.0.6" (#260) by @kbond - -## [v1.17.0](https://github.com/zenstruck/foundry/releases/tag/v1.17.0) - -March 24th, 2022 - [v1.16.0...v1.17.0](https://github.com/zenstruck/foundry/compare/v1.16.0...v1.17.0) - -* c131715 [bug] fix global state with symfony/framework-bundle >= 5.4.6/6.0.6 (#259) by @kbond -* 0edbea8 [minor] remove Symfony 5.3 from test matrix (#259) by @kbond -* 5768345 [feature] add Story "pools" (#252) by @kbond -* be6b6c8 Revert "[feature] Allow any type for Story States (#231)" (#252) by @kbond -* 02cd0c8 [minor] deprecate `Story:add()` and add `Story::addState()` (#254) by @kbond -* 02609a9 [minor] add return type for stub command (deprecated in symfony 6) (#257) by @Chris53897, Christopher Georg -* 6977f3a [doc] Use `UserPasswordHasherInterface` instead of `UserPasswordEncoderInterface` (#255) by @zairigimad -* 01ebfab [feature] add an 'All' option to make:factory to create all missing factories (#247) by @abeal-hottomali -* 39fa8e2 [bug] ignore abstract classes in the maker (#249) by @abeal-hottomali -* 62eeb75 [minor] run php-cs-fixer on php 7.2 (#243) by @kbond - -## [v1.16.0](https://github.com/zenstruck/foundry/releases/tag/v1.16.0) - -January 6th, 2022 - [v1.15.0...v1.16.0](https://github.com/zenstruck/foundry/compare/v1.15.0...v1.16.0) - -* 79261d6 [feature] MongoDB ODM Support (#153) by @kbond, @nikophil -* d97d895 [minor] fix psalm (#232) by @kbond -* fc74f26 [minor] add allow-plugins for composer 2.2+ (#232) by @kbond - -## [v1.15.0](https://github.com/zenstruck/foundry/releases/tag/v1.15.0) - -December 30th, 2021 - [v1.14.1...v1.15.0](https://github.com/zenstruck/foundry/compare/v1.14.1...v1.15.0) - -* fb79022 [feature] Allow any type for Story States (#231) by @wouterj -* d6d7d52 [doc] update url (#230) by @bfoks -* 4915b61 [doc] Fix event hook argument name (#229) by @Aeet, @kbond -* 7e13ed0 [doc] add note about how attributes are built (#228) by @gnito-org -* 552dc6f [doc] Correct spelling in index.rst (#226) by @gnito-org -* 50a91b9 [bug] Fix smallint generated Faker (#223) by @jmsche -* 68552a7 [doc] Document the MakeFactory all-fields option (#220) by @gnito-org -* 93a2f9c [feature] Add all-fields option to MakeFactory (#218) by @gnito-org - -## [v1.14.1](https://github.com/zenstruck/foundry/releases/tag/v1.14.1) - -December 2nd, 2021 - [v1.14.0...v1.14.1](https://github.com/zenstruck/foundry/compare/v1.14.0...v1.14.1) - -* bf1cbc9 [minor] Bump symfony/http-kernel from 5.3.7 to 5.4.0 in /bin/tools/psalm (#217) by @dependabot[bot], dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> -* 433f9b2 [minor] allow symfony/deprecation-contracts 3+ by @kbond -* 2b10729 [minor] fix failing test by @kbond -* b3ce03f [minor] add void return type (#176) by @seb-jean - -## [v1.14.0](https://github.com/zenstruck/foundry/releases/tag/v1.14.0) - -November 13th, 2021 - [v1.13.4...v1.14.0](https://github.com/zenstruck/foundry/compare/v1.13.4...v1.14.0) - -* 46e968e [minor] add Symfony 6.0/PHP 8.1 support (#198) by @kbond - -## [v1.13.4](https://github.com/zenstruck/foundry/releases/tag/v1.13.4) - -October 21st, 2021 - [v1.13.3...v1.13.4](https://github.com/zenstruck/foundry/compare/v1.13.3...v1.13.4) - -* 676f00a [bug] disable migration transactions (#207) by @kbond -* c6df43d [ci] re-enable migration tests on php >= 8 (#207) by @kbond -* 2ef6c6a [ci] disable migration tests on php >= 8 (#206) by @kbond -* e29a008 [bug] fix maker auto-defaults with yaml driver (#205) by @domagoj-orioly -* c483b2e [ci] use reusable workflows where possible (#203) by @kbond -* 886204f [minor] adjust CI output by @kbond -* 63a956e [minor] use zenstruck/assert for assertions instead of phpunit (#182) by @kbond - -## [v1.13.3](https://github.com/zenstruck/foundry/releases/tag/v1.13.3) - -September 24th, 2021 - [v1.13.2...v1.13.3](https://github.com/zenstruck/foundry/compare/v1.13.2...v1.13.3) - -* 477db0a [minor] install psalm as composer-bin tool (#199) by @kbond -* 6ced887 [minor] add Symfony 5.4 to test matrix (#197) by @kbond -* 6610c5d [bug] rename "rank" as it's a reserved keyword in mysql 8 (#197) by @kbond - -## [v1.13.2](https://github.com/zenstruck/foundry/releases/tag/v1.13.2) - -September 3rd, 2021 - [v1.13.1...v1.13.2](https://github.com/zenstruck/foundry/compare/v1.13.1...v1.13.2) - -* 06b24d4 [bug] when creating collections, check for is persisting first (#195) by @jordisala1991, @kbond - -## [v1.13.1](https://github.com/zenstruck/foundry/releases/tag/v1.13.1) - -August 31st, 2021 - [v1.13.0...v1.13.1](https://github.com/zenstruck/foundry/compare/v1.13.0...v1.13.1) - -* 1dccda1 [bug] fix/improve embeddable support (#193) by @kbond -* 5f39d8a [minor] update symfony-tools/docs-builder by @kbond - -## [v1.13.0](https://github.com/zenstruck/foundry/releases/tag/v1.13.0) - -August 30th, 2021 - [v1.12.0...v1.13.0](https://github.com/zenstruck/foundry/compare/v1.12.0...v1.13.0) - -* 39f69f9 [doc] update symfony.com links (#191) by @kbond -* 31b7569 [doc] switch documentation to symfony.com bundle doc format (#190) by @kbond, @wouterj -* d4943d7 [minor] exclude maker templates from phpunit code coverage (#189) by @kbond -* 60f78b3 [doc] add note about simplified factory annotations in PhpStorm 2021.2+ (#189) by @kbond -* 8769d7a [minor] update factory maker template annotations (#189) by @kbond -* 1faf97d [feature] persisting factories respect cascade persist (#181) by @mpiot -* 0416dc4 [minor] Be able to remove most of method annotations on user factories (#185) by @Nyholm -* 468e80b [minor] add missing ->expectDeprecation() to legacy tests (#188) by @kbond -* 1647e1b [bug] ensure legacy test works as expected (#187) by @kbond -* a6f6413 [minor] Added .editorconfig to sync up styles (#186) by @Nyholm -* 2517f54 [minor] change Instantiator::$forceProperties type-hint (#183) by @kbond -* b145059 [minor] psalm fix (#180) by @kbond - -## [v1.12.0](https://github.com/zenstruck/foundry/releases/tag/v1.12.0) - -July 6th, 2021 - [v1.11.1...v1.12.0](https://github.com/zenstruck/foundry/compare/v1.11.1...v1.12.0) - -* 6b97f0f [minor] refactor make:factory auto-default feature (#174) by @kbond -* 2a0bbce [feature] Auto populate ModelFactory::getDefaults() from doctrine mapping (#173) by @benblub - -## [v1.11.1](https://github.com/zenstruck/foundry/releases/tag/v1.11.1) - -June 25th, 2021 - [v1.11.0...v1.11.1](https://github.com/zenstruck/foundry/compare/v1.11.0...v1.11.1) - -* ccac05c [minor] disable codecov pr annotations (#172) by @kbond -* 5c3abe2 [bug] allow passing full namespace with make:factory (#171) by @kbond - -## [v1.11.0](https://github.com/zenstruck/foundry/releases/tag/v1.11.0) - -June 4th, 2021 - [v1.10.0...v1.11.0](https://github.com/zenstruck/foundry/compare/v1.10.0...v1.11.0) - -* d2cd4c7 [feature] customize namespace for factories generated with make:factory (#164) by @kbond -* b9c161b [minor] suppress psalm error (#165) by @kbond -* 112d57d [feature] make:factory only lists entities w/o factories (#162) by @jschaedl -* 6001e7e [minor] Detect missing maker bundle and suggest installation via StubCommands (#161) by @jschaedl -* cdab96e [minor] upgrade to php-cs-fixer 3 (#159) by @kbond -* d357215 [minor] update php-cs-fixer config (#159) by @kbond -* 3ce7da0 [minor] Update .gitattributes file (#158) by @ker0x -* be1d899 [doc] Fix example $posts for Attributes section (#155) by @babeuloula - -## [v1.10.0](https://github.com/zenstruck/foundry/releases/tag/v1.10.0) - -April 19th, 2021 - [v1.9.1...v1.10.0](https://github.com/zenstruck/foundry/compare/v1.9.1...v1.10.0) - -* 7dc49f0 [minor] unlock php-cs-fixer in gh action by @kbond -* e800c83 [feature] add option to use doctrine migrations to reset database (#145) by @kbond -* 8466067 [doc] fix small typo in docs (#147) by @nikophil -* 69fe2a6 [doc] fix typo (#146) by @AntoineRoue -* 463d32a [minor] fix faker deprecations by @kbond -* aa6b32a [minor] lock php-cs-fixer version in ci (bug in latest release) by @kbond -* 4dc13e6 [minor] adjust codecov threshold by @kbond -* cd5cefe [minor] use SHELL_VERBOSITY to hide logs during tests by @kbond - -## [v1.9.1](https://github.com/zenstruck/foundry/releases/tag/v1.9.1) - -March 19th, 2021 - [v1.9.0...v1.9.1](https://github.com/zenstruck/foundry/compare/v1.9.0...v1.9.1) - -* 0ebf0dd [bug] fix false positive for auto-refresh deprecation (fixes #141) (#143) by @kbond -* 7e693ed [doc] remove unnecessary notes about rebooting kernel (ref: #140) (#142) by @kbond -* ba7947c [minor] improve make:factory error message when no entities exist (#139) by @kbond -* 3812def [minor] use project var dir for test cache/logs (#139) by @kbond - -## [v1.9.0](https://github.com/zenstruck/foundry/releases/tag/v1.9.0) - -March 12th, 2021 - [v1.8.0...v1.9.0](https://github.com/zenstruck/foundry/compare/v1.8.0...v1.9.0) - -* 0872be0 [doc] Add --test option as tip (#138) by @OskarStark -* f55afe2 [doc] Fix typos in the docs (#136) by @jdreesen -* 88c081e [minor] "require" explicitly configuring global auto_refresh_proxies (#131) by @kbond -* 632de3d [feature] throw exception during autorefresh if unsaved changes detected (#131) by @kbond -* 5318c7f [minor] deprecate instantiating Factory directly: (#134) by @kbond -* b7a7880 [feature] add AnonymousFactory (#134) by @kbond -* 228895f [minor] add dev stability to ci matrix (#133) by @kbond -* b759712 [minor] explicitly add sqlite extension for gh actions (#130) by @kbond -* 1f332ed [bug] Fix exception message (#129) by @jdreesen -* 4bc80fb [minor] increase codecov threshold by @kbond - -## [v1.8.0](https://github.com/zenstruck/foundry/releases/tag/v1.8.0) - -February 27th, 2021 - [v1.7.1...v1.8.0](https://github.com/zenstruck/foundry/compare/v1.7.1...v1.8.0) - -* 83d6b26 [feature] add ModelFactory::assert()/RepositoryProxy::assert() (#123) by @kbond -* a657d14 [feature] add ModelFactory::all()/find()/findBy() (#123) by @kbond -* ac775b9 [feature] add ModelFactory::count()/truncate() (#123) by @kbond -* 34373da [feature] add ModelFactory::first()/last() (#123) by @kbond -* 5978574 [minor] psalm fixes (#122) by @kbond -* 88cb7c9 [minor] add getCommandDescription() to Maker's (#121) by @kbond -* 31971e0 [minor] fail ci if direct deprecations (#121) by @kbond -* ecc0e10 [bug] bump min php version (fixes #118) (#119) by @kbond - -## [v1.7.1](https://github.com/zenstruck/foundry/releases/tag/v1.7.1) - -February 6th, 2021 - [v1.7.0...v1.7.1](https://github.com/zenstruck/foundry/compare/v1.7.0...v1.7.1) - -* 6bab709 [bug] fix unmanaged many-to-one entity problem (fixes #114) (#117) by @kbond -* b92a69a [minor] adjust cs-check gh action and fix cs (#116) by @kbond - -## [v1.7.0](https://github.com/zenstruck/foundry/releases/tag/v1.7.0) - -January 17th, 2021 - [v1.6.0...v1.7.0](https://github.com/zenstruck/foundry/compare/v1.6.0...v1.7.0) - -* 9d42401 [feature] add attributes to ModelFactory/RepositoryProxy random methods (#112) by @kbond -* 149ea48 [feature] Remove "visual duplication" of ModelFactory::new()->create() (#111) by @wouterj -* 0c69967 [feature] Added ModelFactory::randomOrCreate() (#108) by @wouterj -* 574c246 [minor] use zenstruck/callback for Proxy::executeCallback() (#107) by @kbond -* 07f1ffe [minor] apply suggested psalm fix (#102) by @kbond -* e53b834 [minor] enable code coverage action to work with xdebug 3 (#99) by @kbond -* 6ea273b [minor] psalm-suppress InternalMethod (#99) by @kbond -* 1dbbab8 [minor] add codecov badge (#98) by @kbond -* 77f7ce0 [minor] switch to codecov by @kbond -* f269dc4 [minor] use ramsey/composer-install in static-analysis job (#97) by @kbond -* 0ca5479 [minor] Streamline GitHub CI by using ramsey/composer-install (#96) by @wouterj -* 8511d7a [minor] Re-enable Psalm and fixed annotations (#95) by @wouterj, @kbond - -## [v1.6.0](https://github.com/zenstruck/foundry/releases/tag/v1.6.0) - -December 7th, 2020 - [v1.5.0...v1.6.0](https://github.com/zenstruck/foundry/compare/v1.5.0...v1.6.0) - -* c3f38d2 [minor] use local kernel instance in Factories and ResetDatabase traits (#92) by @kbond -* 88db502 [doc] document the need to create test client before factories (#92) by @kbond -* bf4d47a [bug] ensure foundry isn't rebooted in DatabaseReset (#92) by @kbond -* 1b6231a [bug] ensure kernel shutdown after ResetDatabase::_resetSchema() (#92) by @kbond -* 63c8eb7 [minor] disable psalm static analysis pending fix (#71) by @kbond -* e4d0a06 [doc] Added another relation example to Many-To-One (#93) by @weaverryan -* 596af47 [minor] support php8 (#71) by @kbond -* 2d574a5 [doc] Added docs for ModelFactory::new() (#91) by @Nyholm -* 6bd3195 [doc] Update link to faker (#90) by @Nyholm -* 338f6c8 [minor] Do not turn Psalm PHPdocs into comments (#85) by @wouterj -* dfc4388 [minor] Fixed issues found by Psalm level 4 (#85) by @wouterj -* 99aa22a [minor] Suppress nullable Psalm level 5 error (#85) by @wouterj -* e4ea180 [minor] Fixed issues found by Psalm level 6 (#85) by @wouterj -* 72c2e77 [minor] Added Psalm templated annotations (#85) by @wouterj -* 48572ce [minor] Installed Psalm and configured GitHub Workflow (#85) by @wouterj -* 0476572 [minor] Update docs with PHP file config (#87) by @TavoNiievez -* 66d0025 [minor] Use PHP CS Fixer udiff to only show snippets (#86) by @wouterj -* 8ecb162 [minor] Use consistent spacing in GitHub Actions config (#86) by @wouterj -* 7e90a05 [minor] Only run one build with prefer-lowest (#86) by @wouterj - -## [v1.5.0](https://github.com/zenstruck/foundry/releases/tag/v1.5.0) - -November 10th, 2020 - [v1.4.0...v1.5.0](https://github.com/zenstruck/foundry/compare/v1.4.0...v1.5.0) - -* 3b36c9f [minor] deprecate using snake/kebab-cased attributes (#81) by @kbond -* 3ecd4f6 [minor] set min version of symfony/maker-bundle to 1.13.0 (#81) by @kbond -* 3c0e149 [minor] swap phpunit for phpunit-bridge mark deprecated tests (#81) by @kbond -* 4292d02 [bug] fix typo (#81) by @kbond -* de1fb41 [minor] trigger deprecations for other deprecated code (#81) by @kbond -* 5134347 [minor] deprecate "optional:" & "force:" attribute prefixes (#81) by @kbond -* def8ebc [feature] define extra attributes/forced properties on Instantiator (#81) by @kbond -* da504e0 [bug] boolean nodes should default to false instead of null (#83) by @kbond -* c376fef [minor] sort available entities (#80) by @wouterj -* d3a32cf [minor] add static return annotations (#79) by @micheh -* 77a3583 [minor] run test suite on PostgreSQL (#51) by @kbond -* 8db03c8 [minor] change faker lib used (#70) by @kbond -* 188eb63 [minor] disable dependabot by @kbond -* ea8fefe [minor] bump actions/cache from v1 to v2.1.2 (#69) by @dependabot[bot], dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> -* bd5249d [minor] update shivammathur/setup-php requirement to 2.7.0 (#68) by @dependabot[bot], dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> -* 800fae7 [minor] update actions/checkout requirement to v2.3.3 (#67) by @dependabot[bot], dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> -* 0252e90 [minor] add dependabot for github actions by @kbond -* 537b2fd [bug] RepositoryProxy::findOneBy() with $orderBy checks inner repo (#66) by @kbond -* 9302855 [minor] RepositoryProxy::truncate() compatible with any ObjectManager (#66) by @kbond -* 9fe12e5 [minor] have RepositoryProxy implement \Countable & \IteratorAggregate (#66) by @kbond -* 1c43645 [feature] improve RespositoryProxy::first() and add last() (#64) by @mpiot -* f9418ac [bug] add $orderBy param to RepositoryProxy::findOneBy() (#63) by @mpiot - -## [v1.4.0](https://github.com/zenstruck/foundry/releases/tag/v1.4.0) - -October 20th, 2020 - [v1.3.0...v1.4.0](https://github.com/zenstruck/foundry/compare/v1.3.0...v1.4.0) - -* d5cab62 [doc] fixes (#59) by @kbond -* 4eb546c [doc] document non-Kernel testing (#59) by @kbond -* c8040f7 [doc] document using in PHPUnit data providers (#59) by @kbond -* fce3610 [minor] throw helpful exception if creating service factories w/o boot (#59) by @kbond -* a904e41 [minor] throw helpful exception if using service factories w/o bundle (#59) by @kbond -* 1efc502 [minor] throw helpful exception if using service stories without bundle (#59) by @kbond -* a5d4154 [feature] remove ->withoutPersisting() requirement in pure unit tests (#59) by @kbond -* aef9123 [feature] allow Factories trait to be used in pure units tests (#59) by @kbond -* 732e616 [minor] deprecate TestState::withoutBundle() (#59) by @kbond -* d2c8b47 [bug] ensure Foundry is "shutdown" after each test (#59) by @kbond -* f3cc0c3 [bug] allow model factories to be created in dataProviders (#59) by @kbond -* b80d778 [doc] adding a link to SymfonyCasts (#60) by @weaverryan -* e37f492 [minor] add script to run all test configurations locally by @kbond - -## [v1.3.0](https://github.com/zenstruck/foundry/releases/tag/v1.3.0) +## [v2.0.7](https://github.com/zenstruck/foundry/releases/tag/v2.0.7) -October 14th, 2020 - [v1.2.1...v1.3.0](https://github.com/zenstruck/foundry/compare/v1.2.1...v1.3.0) +July 12th, 2024 - [v2.0.6...v2.0.7](https://github.com/zenstruck/foundry/compare/v2.0.6...v2.0.7) -* fd433b1 [feature] allow factories to be defined as services (#53) by @kbond -* e686a08 [minor] remove dead debug code (#57) by @kbond -* aaf6ab4 [bug] fix typo in Factory stub (fixes #52) (#57) by @kbond +* 5c44991 fix: handle proxies when refreshing entity in Proxy::getState() (#672) by @nikophil +* 49f5e1d Fix faker php urls (#671) by @BackEndTea +* 7719b0d chore(CI): Enable documentation linter (#657) by @cezarpopa -## [v1.2.1](https://github.com/zenstruck/foundry/releases/tag/v1.2.1) +## [v2.0.6](https://github.com/zenstruck/foundry/releases/tag/v2.0.6) -October 12th, 2020 - [v1.2.0...v1.2.1](https://github.com/zenstruck/foundry/compare/v1.2.0...v1.2.1) +July 4th, 2024 - [v2.0.5...v2.0.6](https://github.com/zenstruck/foundry/compare/v2.0.5...v2.0.6) -* ecf674a [doc] note that the ResetDatabase trait is required for global state by @kbond -* b748615 [minor] ensure coverage jobs use dama bundle (#48) by @kbond -* d4e3a2e [bug] sqlite does not support --if-exists (fixes #46) (#48) by @kbond -* 1138cf0 [minor] add sqlite tests (#48) by @kbond -* 91a6032 [minor] adjust github actions to use DATABASE_URL env var (#48) by @kbond - -## [v1.2.0](https://github.com/zenstruck/foundry/releases/tag/v1.2.0) - -October 8th, 2020 - [v1.1.4...v1.2.0](https://github.com/zenstruck/foundry/compare/v1.1.4...v1.2.0) - -* e7b8481 [feature] add FactoryCollection object to help with relationships (#38) by @kbond +* 52ca7b7 fix: only restore error handler for PHPUnit 10 or superior (#668) by @nikophil +* b5090aa docs: Fix broken link to Without Persisting (#660) by @simoheinonen +* 35b0404 feat: re-add Proxy assertions (#663) by @nikophil -## [v1.1.4](https://github.com/zenstruck/foundry/releases/tag/v1.1.4) +## [v2.0.5](https://github.com/zenstruck/foundry/releases/tag/v2.0.5) -October 7th, 2020 - [v1.1.3...v1.1.4](https://github.com/zenstruck/foundry/compare/v1.1.3...v1.1.4) +July 3rd, 2024 - [v2.0.4...v2.0.5](https://github.com/zenstruck/foundry/compare/v2.0.4...v2.0.5) -* 60e6881 [bug] allow RepositoryProxy::proxyResult() to handle doctrine proxies (#43) by @kbond +* 6105a36 fix: make proxy work with last symfony/var-exporter version (#664) by @nikophil +* e8623a3 [DOC] Fix Upgrade Guide URL Rendering (#654) by @cezarpopa +* f7f133a fix: create ArrayCollection if needed (#645) by @nikophil +* 779bee4 fix: after_flush() can use objects created in global state (#653) by @nikophil +* 72e48bf tests(ci): add test permutation for PHPUnit >= 10 (#647) by @nikophil +* 1edf948 docs: fix incoherence (#652) by @nikophil +* 1c66e39 minor: improve repository assertion messages (#651) by @nikophil -## [v1.1.3](https://github.com/zenstruck/foundry/releases/tag/v1.1.3) +## [v2.0.4](https://github.com/zenstruck/foundry/releases/tag/v2.0.4) -September 28th, 2020 - [v1.1.2...v1.1.3](https://github.com/zenstruck/foundry/compare/v1.1.2...v1.1.3) +June 20th, 2024 - [v2.0.3...v2.0.4](https://github.com/zenstruck/foundry/compare/v2.0.3...v2.0.4) -* 118186d [bug] ensure all attributes passed to afterPersist events (fixes #31) (#40) by @kbond -* f054e3c [bug] allow array callables in Proxy::executeCallback() (#39) by @kbond +* 0989c5d fix: don't try to proxify objects that are not persistable (#646) by @nikophil +* 50ae3dc fix: handle contravariance problem when proxifying class with unserialize method (#644) by @nikophil -## [v1.1.2](https://github.com/zenstruck/foundry/releases/tag/v1.1.2) +## [v2.0.3](https://github.com/zenstruck/foundry/releases/tag/v2.0.3) -September 8th, 2020 - [v1.1.1...v1.1.2](https://github.com/zenstruck/foundry/compare/v1.1.1...v1.1.2) +June 19th, 2024 - [v2.0.2...v2.0.3](https://github.com/zenstruck/foundry/compare/v2.0.2...v2.0.3) -* fb5b4ff [minor] run php-cs-fixer self-update (#33) by @kbond -* d734536 [minor] allow doctrine/persistence v2 (#33) by @kbond +* 6f0835f fix(2.x): only reset error handler in before class hook (#643) by @nikophil +* 3c31193 test: add test with multiple ORM schemas (#629) by @vincentchalamon +* 303211a fix: unproxy args in proxy objects (#635) by @nikophil -## [v1.1.1](https://github.com/zenstruck/foundry/releases/tag/v1.1.1) +## [v2.0.2](https://github.com/zenstruck/foundry/releases/tag/v2.0.2) -July 24th, 2020 - [v1.1.0...v1.1.1](https://github.com/zenstruck/foundry/compare/v1.1.0...v1.1.1) +June 14th, 2024 - [v2.0.1...v2.0.2](https://github.com/zenstruck/foundry/compare/v2.0.1...v2.0.2) -* 91af450 [doc] better document without persisting usage (closes #22) (#27) by @kbond -* 03bce0d [minor] improve "foundry not booted" exception message (closes #24) (#28) by @kbond -* 2d7bc47 [doc] fix test example (closes #25) (#26) by @kbond -* 0a4fa3a Update README.md (#23) by @kbond -* 576858a [doc] fix typo (#20) by @jdreesen -* 331716a [doc] add packagist version badge by @kbond +* b76c294 fix(2.x): support Symfony 7.1 (#622) by @nikophil +* 9cd97b7 docs: Improve DX for tests (#636) by @matthieumota +* 17b0228 fix(2.x): add back second parameter for after persist callbacks (#631) by @nikophil +* 0c7b3af docs: Fix typo in the upgrade guide (#624) by @stof +* 933ebbd docs: upgrade readme with a link to upgrade guide (#620) by @nikophil -## [v1.1.0](https://github.com/zenstruck/foundry/releases/tag/v1.1.0) +## [v2.0.1](https://github.com/zenstruck/foundry/releases/tag/v2.0.1) -July 11th, 2020 - [v1.0.0...v1.1.0](https://github.com/zenstruck/foundry/compare/v1.0.0...v1.1.0) +June 10th, 2024 - [v2.0.0...v2.0.1](https://github.com/zenstruck/foundry/compare/v2.0.0...v2.0.1) -* 7d91e42 [minor] change composer "type" by @kbond -* c01374a [BC BREAK] moved bundle to src root so it can be auto-configured by flex by @kbond +* 5f0ce76 Fix `Instantiator::allowExtra` example (#616) by @norkunas +* c2cbcbc fix(orm): reset database instead of dropping the schema when using migrations (#615) by @vincentchalamon -## [v1.0.0](https://github.com/zenstruck/foundry/releases/tag/v1.0.0) +## [v2.0.0](https://github.com/zenstruck/foundry/releases/tag/v2.0.0) -July 10th, 2020 - _[Initial Release](https://github.com/zenstruck/foundry/commits/v1.0.0)_ +June 7th, 2024 - _[Initial Release](https://github.com/zenstruck/foundry/commits/v2.0.0)_ diff --git a/README.md b/README.md index 4472e2796..153eb6871 100644 --- a/README.md +++ b/README.md @@ -47,20 +47,15 @@ $ docker compose up --detach # install dependencies $ composer update -# run test suite with all available permutations -$ composer test - -# run only one permutation +# run main testsuite (with "schema" reset database strategy) +$ composer test-schema +# or $ ./phpunit -# run test suite with dama/doctrine-test-bundle -$ USE_DAMA_DOCTRINE_TEST_BUNDLE=1 vendor/bin/phpunit - -# run test suite with postgreSQL instead of MySQL -$ DATABASE_URL="postgresql://zenstruck:zenstruck@127.0.0.1:5433/zenstruck_foundry?serverVersion=15" vendor/bin/phpunit - -# run test suite with another PHPUnit version -$ PHPUNIT_VERSION=10 vendor/bin/phpunit +# run "migrate" testsuite (with "migrate" reset database strategy) +$ composer test-migrate +# or +$ ./phpunit --testsuite reset-database --bootstrap tests/bootstrap-reset-database.php ``` ### Overriding the default configuration @@ -69,11 +64,20 @@ You can override default environment variables by creating a `.env.local` file, ```bash # .env.local -DATABASE_URL="postgresql://zenstruck:zenstruck@127.0.0.1:5433/zenstruck_foundry?serverVersion=15" # enables postgreSQL instead of MySQL + +# change the database to postgreSQL... +DATABASE_URL="postgresql://zenstruck:zenstruck@127.0.0.1:5433/zenstruck_foundry?serverVersion=15" +# ...or to SQLite +DATABASE_URL="sqlite:///%kernel.project_dir%/var/data.db" + MONGO_URL="" # disables Mongo USE_DAMA_DOCTRINE_TEST_BUNDLE="1" # enables dama/doctrine-test-bundle PHPUNIT_VERSION="11" # possible values: 9, 10, 11, 11.4 +# test reset database with migrations, +# only relevant for "reset-database" testsuite +MIGRATION_CONFIGURATION_FILE="tests/Fixture/MigrationTests/configs/migration-configuration.php" + # run test suite with postgreSQL $ vendor/bin/phpunit ``` diff --git a/UPGRADE-2.0.md b/UPGRADE-2.0.md index 9eb13b5e1..a3d5801cd 100644 --- a/UPGRADE-2.0.md +++ b/UPGRADE-2.0.md @@ -53,7 +53,7 @@ use Zenstruck\Foundry\Utils\Rector\FoundrySetList; return RectorConfig::configure() ->withPaths([ // add all paths where your factories are defined and where Foundry is used - 'src/Factory' + 'src/Factory', 'tests' ]) ->withSets([FoundrySetList::UP_TO_FOUNDRY_2]) @@ -92,6 +92,8 @@ If the mapping is defined outside the code, with xml, yaml or php configuration, 1. Create a `tests/object-manager.php` file which will expose your doctrine config. Here is an example: ```php +object()` (or, now, `_real()`) everywhere to satisf - `enableAutoRefresh()` -> `_enableAutoRefresh()` - `disableAutoRefresh()` -> `_disableAutoRefresh()` - `withoutAutoRefresh()` -> `_withoutAutoRefresh()` - - `assertPersisted()` -> `_assertPersisted()` + - `assertPersisted()` -> `_assertPersisted()` - `assertNotPersisted()` -> `_assertNotPersisted()` - `isPersisted()` is removed without any replacement - `forceSetAll()` is removed without any replacement diff --git a/bin/tools/phpstan/composer.json b/bin/tools/phpstan/composer.json index 244f67f8a..08f7f8015 100644 --- a/bin/tools/phpstan/composer.json +++ b/bin/tools/phpstan/composer.json @@ -1,11 +1,11 @@ { "require": { - "ekino/phpstan-banned-code": "^1.0", - "phpstan/phpstan": "^1.9", - "phpstan/phpstan-symfony": "^1.2", - "phpstan/extension-installer": "1.2", - "phpstan/phpstan-doctrine": "^1.3", - "phpstan/phpstan-phpunit": "^1.4" + "ekino/phpstan-banned-code": "^3.0", + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-symfony": "^2.0", + "phpstan/extension-installer": "^1.2", + "phpstan/phpstan-doctrine": "^2.0", + "phpstan/phpstan-phpunit": "^2.0" }, "config": { "allow-plugins": { diff --git a/bin/tools/phpstan/composer.lock b/bin/tools/phpstan/composer.lock index 153ae99e8..d1e48b6e7 100644 --- a/bin/tools/phpstan/composer.lock +++ b/bin/tools/phpstan/composer.lock @@ -4,43 +4,43 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "40a8977707f373834de0ae705ccfa2b9", + "content-hash": "4071e6b6a36e43a6da55d4160cc31dfc", "packages": [ { "name": "ekino/phpstan-banned-code", - "version": "v1.0.0", + "version": "v3.0.0", "source": { "type": "git", "url": "https://github.com/ekino/phpstan-banned-code.git", - "reference": "4f0d7c8a0c9f5d222ffc24234aa6c5b3b71bf4c3" + "reference": "27122aa1783d6521e500c0c397c53244cfbde26f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ekino/phpstan-banned-code/zipball/4f0d7c8a0c9f5d222ffc24234aa6c5b3b71bf4c3", - "reference": "4f0d7c8a0c9f5d222ffc24234aa6c5b3b71bf4c3", + "url": "https://api.github.com/repos/ekino/phpstan-banned-code/zipball/27122aa1783d6521e500c0c397c53244cfbde26f", + "reference": "27122aa1783d6521e500c0c397c53244cfbde26f", "shasum": "" }, "require": { - "php": "^7.3 || ^8.0", - "phpstan/phpstan": "^1.0" + "php": "^8.1", + "phpstan/phpstan": "^2.0" }, "require-dev": { "ergebnis/composer-normalize": "^2.6", "friendsofphp/php-cs-fixer": "^3.0", "nikic/php-parser": "^4.3", - "phpstan/phpstan-phpunit": "^1.0", + "phpstan/phpstan-phpunit": "^2.0", "phpunit/phpunit": "^9.5", "symfony/var-dumper": "^5.0" }, "type": "phpstan-extension", "extra": { - "branch-alias": { - "dev-master": "1.0-dev" - }, "phpstan": { "includes": [ "extension.neon" ] + }, + "branch-alias": { + "dev-master": "1.0-dev" } }, "autoload": { @@ -63,32 +63,33 @@ "homepage": "https://github.com/ekino/phpstan-banned-code", "keywords": [ "PHPStan", - "code quality" + "code quality", + "static analysis" ], "support": { "issues": "https://github.com/ekino/phpstan-banned-code/issues", - "source": "https://github.com/ekino/phpstan-banned-code/tree/v1.0.0" + "source": "https://github.com/ekino/phpstan-banned-code/tree/v3.0.0" }, - "time": "2021-11-02T08:37:34+00:00" + "time": "2024-11-13T09:57:22+00:00" }, { "name": "phpstan/extension-installer", - "version": "1.2.0", + "version": "1.4.3", "source": { "type": "git", "url": "https://github.com/phpstan/extension-installer.git", - "reference": "f06dbb052ddc394e7896fcd1cfcd533f9f6ace40" + "reference": "85e90b3942d06b2326fba0403ec24fe912372936" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/extension-installer/zipball/f06dbb052ddc394e7896fcd1cfcd533f9f6ace40", - "reference": "f06dbb052ddc394e7896fcd1cfcd533f9f6ace40", + "url": "https://api.github.com/repos/phpstan/extension-installer/zipball/85e90b3942d06b2326fba0403ec24fe912372936", + "reference": "85e90b3942d06b2326fba0403ec24fe912372936", "shasum": "" }, "require": { "composer-plugin-api": "^2.0", "php": "^7.2 || ^8.0", - "phpstan/phpstan": "^1.8.0" + "phpstan/phpstan": "^1.9.0 || ^2.0" }, "require-dev": { "composer/composer": "^2.0", @@ -109,28 +110,32 @@ "MIT" ], "description": "Composer plugin for automatic installation of PHPStan extensions", + "keywords": [ + "dev", + "static analysis" + ], "support": { "issues": "https://github.com/phpstan/extension-installer/issues", - "source": "https://github.com/phpstan/extension-installer/tree/1.2.0" + "source": "https://github.com/phpstan/extension-installer/tree/1.4.3" }, - "time": "2022-10-17T12:59:16+00:00" + "time": "2024-09-04T20:21:43+00:00" }, { "name": "phpstan/phpstan", - "version": "1.12.7", + "version": "2.0.3", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "dc2b9976bd8b0f84ec9b0e50cc35378551de7af0" + "reference": "46b4d3529b12178112d9008337beda0cc2a1a6b4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/dc2b9976bd8b0f84ec9b0e50cc35378551de7af0", - "reference": "dc2b9976bd8b0f84ec9b0e50cc35378551de7af0", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/46b4d3529b12178112d9008337beda0cc2a1a6b4", + "reference": "46b4d3529b12178112d9008337beda0cc2a1a6b4", "shasum": "" }, "require": { - "php": "^7.2|^8.0" + "php": "^7.4|^8.0" }, "conflict": { "phpstan/phpstan-shim": "*" @@ -171,25 +176,25 @@ "type": "github" } ], - "time": "2024-10-18T11:12:07+00:00" + "time": "2024-11-28T22:19:37+00:00" }, { "name": "phpstan/phpstan-doctrine", - "version": "1.5.3", + "version": "2.0.1", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-doctrine.git", - "reference": "38db3bad8f1567d7bf64806738d724261f8a2b5c" + "reference": "bdb6a835c5aa9725979694ae9b70591e180f4853" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-doctrine/zipball/38db3bad8f1567d7bf64806738d724261f8a2b5c", - "reference": "38db3bad8f1567d7bf64806738d724261f8a2b5c", + "url": "https://api.github.com/repos/phpstan/phpstan-doctrine/zipball/bdb6a835c5aa9725979694ae9b70591e180f4853", + "reference": "bdb6a835c5aa9725979694ae9b70591e180f4853", "shasum": "" }, "require": { - "php": "^7.2 || ^8.0", - "phpstan/phpstan": "^1.11.7" + "php": "^7.4 || ^8.0", + "phpstan/phpstan": "^2.0.3" }, "conflict": { "doctrine/collections": "<1.0", @@ -202,21 +207,20 @@ "cache/array-adapter": "^1.1", "composer/semver": "^3.3.2", "cweagans/composer-patches": "^1.7.3", - "doctrine/annotations": "^1.11 || ^2.0", + "doctrine/annotations": "^2.0", "doctrine/collections": "^1.6 || ^2.1", "doctrine/common": "^2.7 || ^3.0", - "doctrine/dbal": "^2.13.8 || ^3.3.3", + "doctrine/dbal": "^3.3.8", "doctrine/lexer": "^2.0 || ^3.0", - "doctrine/mongodb-odm": "^1.3 || ^2.4.3", + "doctrine/mongodb-odm": "^2.4.3", "doctrine/orm": "^2.16.0", "doctrine/persistence": "^2.2.1 || ^3.2", "gedmo/doctrine-extensions": "^3.8", "nesbot/carbon": "^2.49", - "nikic/php-parser": "^4.13.2", "php-parallel-lint/php-parallel-lint": "^1.2", - "phpstan/phpstan-phpunit": "^1.3.13", - "phpstan/phpstan-strict-rules": "^1.5.1", - "phpunit/phpunit": "^9.6.16", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^9.6.20", "ramsey/uuid": "^4.2", "symfony/cache": "^5.4" }, @@ -241,36 +245,35 @@ "description": "Doctrine extensions for PHPStan", "support": { "issues": "https://github.com/phpstan/phpstan-doctrine/issues", - "source": "https://github.com/phpstan/phpstan-doctrine/tree/1.5.3" + "source": "https://github.com/phpstan/phpstan-doctrine/tree/2.0.1" }, - "time": "2024-09-01T13:17:34+00:00" + "time": "2024-12-02T16:48:00+00:00" }, { "name": "phpstan/phpstan-phpunit", - "version": "1.4.0", + "version": "2.0.1", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-phpunit.git", - "reference": "f3ea021866f4263f07ca3636bf22c64be9610c11" + "reference": "4b6ad7fab8683ff4efd7887ba26ef8ee171c7475" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-phpunit/zipball/f3ea021866f4263f07ca3636bf22c64be9610c11", - "reference": "f3ea021866f4263f07ca3636bf22c64be9610c11", + "url": "https://api.github.com/repos/phpstan/phpstan-phpunit/zipball/4b6ad7fab8683ff4efd7887ba26ef8ee171c7475", + "reference": "4b6ad7fab8683ff4efd7887ba26ef8ee171c7475", "shasum": "" }, "require": { - "php": "^7.2 || ^8.0", - "phpstan/phpstan": "^1.11" + "php": "^7.4 || ^8.0", + "phpstan/phpstan": "^2.0" }, "conflict": { "phpunit/phpunit": "<7.0" }, "require-dev": { - "nikic/php-parser": "^4.13.0", "php-parallel-lint/php-parallel-lint": "^1.2", - "phpstan/phpstan-strict-rules": "^1.5.1", - "phpunit/phpunit": "^9.5" + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^9.6" }, "type": "phpstan-extension", "extra": { @@ -293,38 +296,37 @@ "description": "PHPUnit extensions and rules for PHPStan", "support": { "issues": "https://github.com/phpstan/phpstan-phpunit/issues", - "source": "https://github.com/phpstan/phpstan-phpunit/tree/1.4.0" + "source": "https://github.com/phpstan/phpstan-phpunit/tree/2.0.1" }, - "time": "2024-04-20T06:39:00+00:00" + "time": "2024-11-12T12:48:00+00:00" }, { "name": "phpstan/phpstan-symfony", - "version": "1.4.10", + "version": "2.0.0", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-symfony.git", - "reference": "f7d5782044bedf93aeb3f38e09c91148ee90e5a1" + "reference": "1ef4dce2baabd464c2dd3109d051bad94efa1e79" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-symfony/zipball/f7d5782044bedf93aeb3f38e09c91148ee90e5a1", - "reference": "f7d5782044bedf93aeb3f38e09c91148ee90e5a1", + "url": "https://api.github.com/repos/phpstan/phpstan-symfony/zipball/1ef4dce2baabd464c2dd3109d051bad94efa1e79", + "reference": "1ef4dce2baabd464c2dd3109d051bad94efa1e79", "shasum": "" }, "require": { "ext-simplexml": "*", - "php": "^7.2 || ^8.0", - "phpstan/phpstan": "^1.12" + "php": "^7.4 || ^8.0", + "phpstan/phpstan": "^2.0" }, "conflict": { "symfony/framework-bundle": "<3.0" }, "require-dev": { - "nikic/php-parser": "^4.13.0", "php-parallel-lint/php-parallel-lint": "^1.2", - "phpstan/phpstan-phpunit": "^1.3.11", - "phpstan/phpstan-strict-rules": "^1.5.1", - "phpunit/phpunit": "^8.5.29 || ^9.5", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^9.6", "psr/container": "1.0 || 1.1.1", "symfony/config": "^5.4 || ^6.1", "symfony/console": "^5.4 || ^6.1", @@ -365,18 +367,18 @@ "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.4.10" + "source": "https://github.com/phpstan/phpstan-symfony/tree/2.0.0" }, - "time": "2024-09-26T18:14:50+00:00" + "time": "2024-11-06T10:13:40+00:00" } ], "packages-dev": [], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, - "platform": [], - "platform-dev": [], + "platform": {}, + "platform-dev": {}, "plugin-api-version": "2.6.0" } diff --git a/composer.json b/composer.json index aec2e99e4..beeb7cafb 100644 --- a/composer.json +++ b/composer.json @@ -20,26 +20,32 @@ "doctrine/persistence": "^2.0|^3.0", "fakerphp/faker": "^1.23", "symfony/deprecation-contracts": "^2.2|^3.0", + "symfony/event-dispatcher": "^6.4|^7.0", "symfony/framework-bundle": "^6.4|^7.0", "symfony/property-access": "^6.4|^7.0", + "symfony/property-info": "^6.4|^7.0", "symfony/var-exporter": "^6.4.9|~7.0.9|^7.1.2", "zenstruck/assert": "^1.4" }, "require-dev": { "bamarni/composer-bin-plugin": "^1.8", - "dama/doctrine-test-bundle": "^7.0|^8.0", + "brianium/paratest": "^6|^7", + "dama/doctrine-test-bundle": "^8.0", "doctrine/collections": "^1.7|^2.0", - "doctrine/common": "^3.2", + "doctrine/common": "^3.2.2", "doctrine/doctrine-bundle": "^2.10", "doctrine/doctrine-migrations-bundle": "^2.2|^3.0", "doctrine/mongodb-odm-bundle": "^4.6|^5.0", + "doctrine/mongodb-odm": "^2.4", "doctrine/orm": "^2.16|^3.0", "phpunit/phpunit": "^9.5.0 || ^10.0 || ^11.0", "symfony/console": "^6.4|^7.0", "symfony/dotenv": "^6.4|^7.0", "symfony/maker-bundle": "^1.55", "symfony/phpunit-bridge": "^6.4|^7.0", + "symfony/runtime": "^6.4|^7.0", "symfony/translation-contracts": "^3.4", + "symfony/uid": "^6.4|^7.0", "symfony/var-dumper": "^6.4|^7.0", "symfony/yaml": "^6.4|^7.0" }, @@ -48,21 +54,27 @@ "Zenstruck\\Foundry\\": "src/", "Zenstruck\\Foundry\\Psalm\\": "utils/psalm" }, - "files": ["src/functions.php", "src/Persistence/functions.php", "src/phpunit_helper.php"] + "files": [ + "src/functions.php", + "src/Persistence/functions.php", + "src/symfony_console.php" + ] }, "autoload-dev": { "psr-4": { "Zenstruck\\Foundry\\Tests\\": ["tests/"], "App\\": "tests/Fixture/Maker/tmp/src", "App\\Tests\\": "tests/Fixture/Maker/tmp/tests" - } + }, + "exclude-from-classmap": ["tests/Fixture/Maker/expected"] }, "config": { "preferred-install": "dist", "sort-packages": true, "allow-plugins": { "bamarni/composer-bin-plugin": true, - "symfony/flex": true + "symfony/flex": true, + "symfony/runtime": false } }, "extra": { @@ -77,22 +89,15 @@ }, "scripts": { "test": [ - "@test-schema-no-dama", - "@test-migrate-no-dama", - "@test-schema-dama", - "@test-migrate-dama" + "@test-main", + "@test-reset-database" ], - "test-schema-no-dama": "DATABASE_RESET_MODE=schema USE_DAMA_DOCTRINE_TEST_BUNDLE=0 ./phpunit", - "test-migrate-no-dama": "DATABASE_RESET_MODE=migrate USE_DAMA_DOCTRINE_TEST_BUNDLE=0 ./phpunit", - "test-schema-dama": "DATABASE_RESET_MODE=schema USE_DAMA_DOCTRINE_TEST_BUNDLE=1 ./phpunit", - "test-migrate-dama": "DATABASE_RESET_MODE=migrate USE_DAMA_DOCTRINE_TEST_BUNDLE=1 ./phpunit" + "test-main": "./phpunit --testsuite main", + "test-reset-database": "./phpunit --testsuite reset-database --bootstrap tests/bootstrap-reset-database.php" }, "scripts-descriptions": { - "test": "Run all test permutations", - "test-schema-no-dama": "Test with schema reset (no dama/doctrine-test-bundle)", - "test-migrate-no-dama": "Test with migrations reset (no dama/doctrine-test-bundle)", - "test-schema-dama": "Test with schema reset and dama/doctrine-test-bundle", - "test-migrate-dama": "Test with migrations reset and dama/doctrine-test-bundle" + "test-main": "Main test suite", + "test-reset-database": "Test reset database test suite" }, "minimum-stability": "dev", "prefer-stable": true diff --git a/config/in_memory.php b/config/in_memory.php new file mode 100644 index 000000000..2c302988f --- /dev/null +++ b/config/in_memory.php @@ -0,0 +1,18 @@ +services() + ->set('.zenstruck_foundry.in_memory.factory_registry', InMemoryFactoryRegistry::class) + ->decorate('.zenstruck_foundry.factory_registry') + ->arg('$decorated', service('.inner')); + + $container->services() + ->set('.zenstruck_foundry.in_memory.repository_registry', InMemoryRepositoryRegistry::class) + ->arg('$inMemoryRepositories', abstract_arg('inMemoryRepositories')) + ; +}; diff --git a/config/mongo.php b/config/mongo.php index 83ae902fd..172f1bd0d 100644 --- a/config/mongo.php +++ b/config/mongo.php @@ -3,6 +3,7 @@ namespace Symfony\Component\DependencyInjection\Loader\Configurator; use Zenstruck\Foundry\Mongo\MongoPersistenceStrategy; +use Zenstruck\Foundry\Mongo\MongoResetter; use Zenstruck\Foundry\Mongo\MongoSchemaResetter; return static function (ContainerConfigurator $container): void { @@ -13,7 +14,8 @@ abstract_arg('config'), ]) ->tag('.foundry.persistence_strategy') - ->set('.zenstruck_foundry.persistence.schema_resetter.mongo', MongoSchemaResetter::class) + + ->set(MongoResetter::class, MongoSchemaResetter::class) ->args([ abstract_arg('managers'), ]) diff --git a/config/orm.php b/config/orm.php index 1621dc5a8..10c2f95b2 100644 --- a/config/orm.php +++ b/config/orm.php @@ -8,8 +8,7 @@ use Zenstruck\Foundry\ORM\OrmV3PersistenceStrategy; use Zenstruck\Foundry\ORM\ResetDatabase\BaseOrmResetter; use Zenstruck\Foundry\ORM\ResetDatabase\DamaDatabaseResetter; -use Zenstruck\Foundry\ORM\ResetDatabase\SchemaDatabaseResetter; -use Zenstruck\Foundry\ORM\ResetDatabase\MigrateDatabaseResetter; +use Zenstruck\Foundry\ORM\ResetDatabase\OrmResetter; return static function (ContainerConfigurator $container): void { $container->services() @@ -26,13 +25,7 @@ ->arg('$connections', service('connections')) ->abstract() - ->set('.zenstruck_foundry.persistence.database_resetter.orm.schema', SchemaDatabaseResetter::class) - ->parent('.zenstruck_foundry.persistence.database_resetter.orm.abstract') - ->tag('.foundry.persistence.database_resetter') - ->tag('.foundry.persistence.schema_resetter') - - ->set('.zenstruck_foundry.persistence.database_resetter.orm.migrate', MigrateDatabaseResetter::class) - ->arg('$configurations', abstract_arg('configurations')) + ->set(OrmResetter::class, /* class to be defined thanks to the configuration */) ->parent('.zenstruck_foundry.persistence.database_resetter.orm.abstract') ->tag('.foundry.persistence.database_resetter') ->tag('.foundry.persistence.schema_resetter') @@ -40,13 +33,8 @@ if (\class_exists(StaticDriver::class)) { $container->services() - ->set('.zenstruck_foundry.persistence.database_resetter.orm.schema.dama', DamaDatabaseResetter::class) - ->decorate('.zenstruck_foundry.persistence.database_resetter.orm.schema') - ->args([ - service('.inner'), - ]) - ->set('.zenstruck_foundry.persistence.database_resetter.orm.migrate.dama', DamaDatabaseResetter::class) - ->decorate('.zenstruck_foundry.persistence.database_resetter.orm.migrate') + ->set('.zenstruck_foundry.persistence.database_resetter.orm.dama', DamaDatabaseResetter::class) + ->decorate(OrmResetter::class, priority: 10) ->args([ service('.inner'), ]) diff --git a/config/services.php b/config/services.php index cd3fd6fd9..7c1b16e9e 100644 --- a/config/services.php +++ b/config/services.php @@ -32,6 +32,8 @@ service('.zenstruck_foundry.instantiator'), service('.zenstruck_foundry.story_registry'), service('.zenstruck_foundry.persistence_manager')->nullOnInvalid(), + service('event_dispatcher'), + service('.zenstruck_foundry.in_memory.repository_registry'), ]) ->public() ; diff --git a/docker-compose.yaml b/docker-compose.yaml index 0dc41a3bf..0b7e910e1 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,5 +1,6 @@ services: mysql: + platform: linux/x86_64 image: mysql:5.7 ports: - "3307:3306" diff --git a/docs/index.rst b/docs/index.rst index 88a216778..f064f1294 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -11,7 +11,10 @@ Foundry supports ``doctrine/orm`` (with `doctrine/doctrine-bundle `_) or a combination of these. -Want to watch a screencast 🎥 about it? Check out https://symfonycasts.com/foundry +.. admonition:: Screencast + :class: screencast + + Want to watch a screencast 🎥 about it? Check out `symfonycasts.com/foundry `_. .. warning:: @@ -218,8 +221,8 @@ This command will generate a ``PostFactory`` class that looks like this: * @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 first(string $sortBy = 'id') + * @method static Post|Proxy last(string $sortBy = 'id') * @method static Post|Proxy random(array $attributes = []) * @method static Post|Proxy randomOrCreate(array $attributes = [])) * @method static PostRepository|RepositoryProxy repository() @@ -234,8 +237,8 @@ This command will generate a ``PostFactory`` class that looks like this: * @phpstan-method static Proxy&Post createOne(array $attributes = []) * @phpstan-method static Proxy&Post find(object|array|mixed $criteria) * @phpstan-method static Proxy&Post findOrCreate(array $attributes) - * @phpstan-method static Proxy&Post first(string $sortedField = 'id') - * @phpstan-method static Proxy&Post last(string $sortedField = 'id') + * @phpstan-method static Proxy&Post first(string $sortBy = 'id') + * @phpstan-method static Proxy&Post last(string $sortBy = 'id') * @phpstan-method static Proxy&Post random(array $attributes = []) * @phpstan-method static Proxy&Post randomOrCreate(array $attributes = []) * @phpstan-method static list&Post> all() @@ -251,6 +254,8 @@ This command will generate a ``PostFactory`` class that looks like this: // ... } +.. _defaults: + In the ``defaults()``, you can return an array of all default values that any new object should have. `Faker`_ is available to easily get random data: @@ -259,13 +264,22 @@ should have. `Faker`_ is available to easily get random data: protected function defaults(): array { return [ - // Symfony's property-access component is used to populate the properties - // this means that setTitle() will be called or you can have a $title constructor argument + // use the built-in Faker integration to generate good random values... 'title' => self::faker()->unique()->sentence(), 'body' => self::faker()->sentence(), + + // ...or generate the values yourself if you prefer + 'createdAt' => new \DateTimeImmutable('today'), ]; } +These default values are applied to both the **constructor arguments** and the +**properties** of the objects. For example, defining a default value for ``title`` +will first attempt to set a constructor argument called ``$title``. If that doesn't +exist, the `PropertyAccess `_ +component will be used to call the ``setTitle()`` method or directly set the public +``$title`` property. More about this in the :ref:`instantiation and hydration ` section. + .. tip:: It is best to have ``defaults()`` return the attributes to persist a valid object @@ -520,9 +534,7 @@ random data for your factories: .. note:: You can register your own *Faker Provider* by tagging any service with ``foundry.faker_provider``. - All public methods on this service will be available on Foundry's Faker instance: - -:: + All public methods on this service will be available on Foundry's Faker instance:: use function Zenstruck\Foundry\faker; @@ -540,10 +552,10 @@ random data for your factories: faker: service: my_faker # service id for your own instance of Faker\Generator -Events / Hooks -~~~~~~~~~~~~~~ +Hooks +~~~~~ -The following events can be added to factories. Multiple event callbacks can be added, they are run in the order +The following hooks can be added to factories. Multiple hooks callbacks can be added, they are run in the order they were added. :: @@ -552,24 +564,28 @@ they were added. use Zenstruck\Foundry\Proxy; PostFactory::new() - ->beforeInstantiate(function(array $attributes): array { - // $attributes is what will be used to instantiate the object, manipulate as required - $attributes['title'] = 'Different title'; + ->beforeInstantiate(function(array $parameters, string $class, static $factory): array { + // $parameters is what will be used to instantiate the object, manipulate as required + // $class is the class of the object being instantiated + // $factory is the factory instance which creates the object + $parameters['title'] = 'Different title'; - return $attributes; // must return the final $attributes + return $parameters; // must return the final $parameters }) - ->afterInstantiate(function(Post $object, array $attributes): void { + ->afterInstantiate(function(Post $object, array $parameters, static $factory): void { // $object is the instantiated object - // $attributes contains the attributes used to instantiate the object and any extras + // $parameters contains the attributes used to instantiate the object and any extras + // $factory is the factory instance which creates the object }) - ->afterPersist(function(Post $object, array $attributes) { + ->afterPersist(function(Post $object, array $parameters, static $factory) { // this event is only called if the object was persisted // $object is the persisted Post object - // $attributes contains the attributes used to instantiate the object and any extras + // $parameters contains the attributes used to instantiate the object and any extras + // $factory is the factory instance which creates the object }) // multiple events are allowed - ->beforeInstantiate(function($attributes) { return $attributes; }) + ->beforeInstantiate(function($parameters) { return $parameters; }) ->afterInstantiate(function() {}) ->afterPersist(function() {}) ; @@ -587,6 +603,58 @@ You can also add hooks directly in your factory class: Read `Initialization`_ to learn more about the ``initialize()`` method. +Hooks as service / global hooks +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +For a better control of your hooks, you can define them as services, allowing to leverage dependency injection and +to create hooks globally: + +:: + + use Symfony\Component\EventDispatcher\Attribute\AsEventListener; + use Zenstruck\Foundry\Object\Event\AfterInstantiate; + use Zenstruck\Foundry\Object\Event\BeforeInstantiate; + use Zenstruck\Foundry\Persistence\Event\AfterPersist; + + final class FoundryHook + { + #[AsFoundryHook(Post::class)] + public function beforeInstantiate(BeforeInstantiate $event): void + { + // do something before the post is instantiated: + // $event->parameters is what will be used to instantiate the object, manipulate as required + // $event->objectClass is the class of the object being instantiated + // $event->factory is the factory instance which creates the object + } + + #[AsFoundryHook(Post::class)] + public function afterInstantiate(AfterInstantiate $event): void + { + // $event->object is the instantiated Post object + // $event->parameters contains the attributes used to instantiate the object and any extras + // $event->factory is the factory instance which creates the object + } + + #[AsFoundryHook(Post::class)] + public function afterPersist(AfterPersist $event): void + { + // this event is only called if the object was persisted + // $event->object is the persisted Post object + // $event->parameters contains the attributes used to instantiate the object and any extras + // $event->factory is the factory instance which creates the object + } + + #[AsFoundryHook] + public function afterInstantiateGlobal(AfterInstantiate $event): void + { + // Omitting class defines a "global" hook which will be called for all objects + } + } + +.. versionadded:: 2.4 + + The ``#[AsFoundryHook]`` attribute was added in Foundry 2.4. + Initialization ~~~~~~~~~~~~~~ @@ -612,15 +680,16 @@ You can override your factory's ``initialize()`` method to add default state/log .. _instantiation: -Instantiation -~~~~~~~~~~~~~ +Object Instantiation & Hydration +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ By default, objects are instantiated in the normal fashion, by using the object's constructor. Attributes -that match constructor arguments are used. Remaining attributes are set to the object using Symfony's -`PropertyAccess `_ component +that match constructor arguments are used. Remaining attributes are used in the hydration phase and set to the object +using Symfony's `PropertyAccess `_ component (setters/public properties). Any extra attributes cause an exception to be thrown. -You can customize the instantiator in several ways: +You can customize the instantiator in several ways, so that Foundry will instantiate and hydrate your objects, using the +attributes provided: :: @@ -651,16 +720,29 @@ You can customize the instantiator in several ways: // use a "namedConstructor" ->instantiateWith(Instantiator::namedConstructor("methodName")) - // use a callable + // use a callable: it will be passed the attributes matching its parameters names, + // remaining attributes will be used in the hydration phase ->instantiateWith(Instantiator::use(function(string $title): object { - return new Post($title); // ... your own logic + return new Post($title); // ... your own instantiation logic })) + ; + +If this does not suit your needs, the instantiator is just a callable. You can provide your own to have complete control +over instantiation and hydration phases: + +:: - // the instantiator is just a callable, you can provide your own ->instantiateWith(function(array $attributes, string $class): object { return new Post(); // ... your own logic }) - ; + +.. warning:: + + The ``instantiateWith(callable(...))`` method fully replaces the default instantiation + and object hydration system. Attributes defined in the ``defaults()`` method, + as well as any states defined with the ``with()`` method, **will not be + applied automatically**. However, they are available as arguments to the + ``instantiateWith()`` callable. You can customize the instantiator globally for all your factories (can still be overruled by factory instance instantiators): @@ -680,7 +762,7 @@ instantiators): Immutable ~~~~~~~~~ -Factory's are immutable: +Factories are immutable: :: @@ -716,7 +798,7 @@ The following assumes the ``Comment`` entity has a many-to-one relationship with $post = PostFactory::createOne(); // instance of Proxy CommentFactory::createOne(['post' => $post]); - CommentFactory::createOne(['post' => $post->object()]); // functionally the same as above + CommentFactory::createOne(['post' => $post->_real()]); // functionally the same as above // Example 2: pre-create Posts and choose a random one PostFactory::createMany(5); // create 5 Posts @@ -839,8 +921,8 @@ The ``defaults()`` method is called everytime a factory is instantiated (even if creating it). Sometimes, you might not want your value calculated every time. For example, if you have a value for one of your attributes that: - - has side effects (i.e. creating a file or fetching a random existing entity from another factory) - - you only want to calculate once (i.e. creating an entity from another factory to pass as a value into multiple other factories) +* has side effects (i.e. creating a file or fetching a random existing entity from another factory) +* you only want to calculate once (i.e. creating an entity from another factory to pass as a value into multiple other factories) You can wrap the value in a ``LazyValue`` which ensures the value is only calculated when/if it's needed. Additionally, the LazyValue can be `memoized `_ so that it is only calculated once. @@ -1047,7 +1129,7 @@ still wrapped in a ``Proxy`` to optionally save later. $post = PostFactory::new()->withoutPersisting()->create(); // returns Post|Proxy $post->setTitle('something else'); // do something with object - $post->save(); // persist the Post (save() is a method on Proxy) + $post->_save(); // persist the Post (save() is a method on Proxy) $post = PostFactory::new()->withoutPersisting()->create()->object(); // actual Post object @@ -1301,7 +1383,7 @@ speed. When this bundle is enabled, the database is dropped/created and migrated Additionally, it is possible to provide `configuration files `_ to be used by the migrations. The configuration files can be in any format supported by Doctrine Migrations (php, xml, -json, yml). Then then command ``doctrine:migrations:migrate`` will run as many times as the number of configuration +json, yml). Then the command ``doctrine:migrations:migrate`` will run as many times as the number of configuration files. .. configuration-block:: @@ -1321,6 +1403,45 @@ files. - '%kernel.root_dir%/migrations/configuration.php' - 'migrations/configuration.yaml' +Extending reset mechanism +......................... + +The reset mechanism can be extended thanks to decoration: + +:: + + use Symfony\Component\DependencyInjection\Attribute\AsDecorator; + use Symfony\Component\DependencyInjection\Attribute\When; + use Symfony\Component\HttpKernel\KernelInterface; + use Zenstruck\Foundry\ORM\ResetDatabase\OrmResetter; + + // The decorator should be declared in test environment only. + #[When('test')] + // You can also decorate `MongoResetter::class`. + #[AsDecorator(OrmResetter::class)] + final readonly class DecorateDatabaseResetter implements OrmResetter + { + public function __construct( + private OrmResetter $decorated + ) {} + + public function resetBeforeFirstTest(KernelInterface $kernel): void + { + // do something once per test suite (for instance: install a PostgreSQL extension) + + $this->decorated->resetBeforeFirstTest($kernel); + } + + public function resetBeforeEachTest(KernelInterface $kernel): void + { + // do something once per test case (for instance: restart PostgreSQL sequences) + + $this->decorated->resetBeforeEachTest($kernel); + } + } + +If using a standard Symfony Flex app, this will be autowired/autoconfigured. If not, register the service + .. _object-proxy: Object Proxy @@ -1441,7 +1562,9 @@ Proxy objects pitfalls Proxified objects may have some pitfalls when dealing with Doctrine's entity manager. You may encounter this error: -> Doctrine\ORM\ORMInvalidArgumentException: A new entity was found through the relationship +.. code-block:: text + + > Doctrine\ORM\ORMInvalidArgumentException: A new entity was found through the relationship 'App\Entity\Post#category' that was not configured to cascade persist operations for entity: AppEntityCategoryProxy@3082. To solve this issue: Either explicitly call EntityManager#persist() on this unknown entity or configure cascade persist this association in the mapping for example @ManyToOne(..,cascade={"persist"}). If you cannot find out which entity @@ -1450,7 +1573,6 @@ Proxified objects may have some pitfalls when dealing with Doctrine's entity man The problem will occur if a proxy has been passed to ``EntityManager::persist()``. To fix this, you should pass the "real" object, by calling ``$proxyfiedObject->_real()``. - Factory without proxy ..................... @@ -1534,7 +1656,7 @@ Global State ~~~~~~~~~~~~ If you have an initial database state you want for all tests, you can set this in the config of the bundle. Accepted -values are: stories as service, "global" stories and invokable services. Global state is loaded before each using +values are: stories as service, "global" stories and invokable services. Global state is loaded before each test using the ``ResetDatabase`` trait. If you are using `DamaDoctrineTestBundle`_, it is only loaded once for the entire test suite. @@ -1568,7 +1690,61 @@ PHPUnit Data Providers ~~~~~~~~~~~~~~~~~~~~~~ It is possible to use factories in -`PHPUnit data providers `_: +`PHPUnit data providers `_. +Their usage depends on whether you're using Foundry's `PHPUnit Extension`_ or not.: + +With PHPUnit Extension +...................... + +.. versionadded:: 2.2 + + The ability to call ``Factory::create()`` in data providers was introduced in Foundry 2.2. + +.. warning:: + + You will need at least PHPUnit 11.4 to call ``Factory::create()`` in your data providers. + +Thanks to Foundry's `PHPUnit Extension`_, you'll be able to use your factories in your data providers the same way +you're using them in tests. Thanks to it, you can: + +* Call ``->create()`` or ``::createOne()`` or any other method which creates objects in unit tests + (using ``PHPUnit\Framework\TestCase``) and functional tests (``Symfony\Bundle\FrameworkBundle\Test\KernelTestCase``); +* Use `Factories as Services`_ in functional tests; +* Use ``faker()`` normally, without wrapping its call in a callable. + +:: + + use App\Factory\PostFactory; + use PHPUnit\Framework\Attributes\DataProvider; + + #[DataProvider('createMultipleObjectsInDataProvider')] + public function test_post_via_data_provider(Post $post): void + { + // at this point, `$post` exists, and is already stored in database + } + + public static function postDataProvider(): iterable + { + yield [PostFactory::createOne()]; + yield [PostWithServiceFactory::createOne()]; + yield [PostFactory::createOne(['body' => faker()->sentence()]; + } + +.. warning:: + + Because Foundry is relying on its `Proxy mechanism `_, when using persistence, + your factories must extend ``Zenstruck\Foundry\Persistence\PersistentProxyObjectFactory`` to work in your data providers. + +.. warning:: + + For the same reason, you should not call methods from `Proxy` class in your data providers, not even ``->_real()``. + + +Without PHPUnit Extension +......................... + +Data providers are computed early in the phpunit process before Foundry is booted. +Be sure your data provider returns only instances of ``Factory`` and you do not try to call ``->create()`` on them: :: @@ -1590,11 +1766,6 @@ It is possible to use factories in yield [PostFactory::new()->published()]; } -.. note:: - - Be sure your data provider returns only instances of ``Factory`` and you do not try to call ``->create()`` on them. - Data providers are computed early in the phpunit process before Foundry is booted. - .. note:: For the same reason as above, it is not possible to use `Factories as Services`_ with required @@ -1651,7 +1822,7 @@ It is possible to use factories in )->asDataProvider(); } - The ``FactoryCollection`` could also be passed directly to the test case in order to have several objects available in the same test: +The ``FactoryCollection`` could also be passed directly to the test case in order to have several objects available in the same test: :: @@ -1725,7 +1896,7 @@ This can dramatically improve test speed. The following considerations need to b .. code-block:: yaml - # config/packages/doctrine.yaml + # config/packages/doctrine.yaml when@test: doctrine: dbal: @@ -1938,24 +2109,19 @@ If your stories require dependencies, you can define them as a service: namespace App\Story; use App\Factory\PostFactory; - use App\Service\ServiceA; - use App\Service\ServiceB; + use App\Service\MyService; use Zenstruck\Foundry\Story; final class PostStory extends Story { - private $serviceA; - private $serviceB; - - public function __construct(ServiceA $serviceA, ServiceB $serviceB) - { - $this->serviceA = $serviceA; - $this->serviceB = $serviceB; + public function __construct( + private MyService $myService, + ) { } public function build(): void { - // can use $this->serviceA, $this->serviceB here to help build this story + // $this->myService can be used here to help build this story } } @@ -2006,10 +2172,11 @@ Later, you can access the story's state when creating other fixtures: namespace App\Story; use App\Factory\CategoryFactory; + use Zenstruck\Foundry\Persistence\Proxy; use Zenstruck\Foundry\Story; /** - * @method static Category php() + * @method static Category&Proxy php() */ final class CategoryStory extends Story { @@ -2067,6 +2234,39 @@ Objects can be fetched from pools in your tests, fixtures or other stories: ProvinceStory::getRandomRange('be', 1, 4); // between 1 and 4 random Province|Proxy's from "be" pool ProvinceStory::getPool('be'); // all Province|Proxy's from "be" pool +#[WithStory] Attribute +~~~~~~~~~~~~~~~~~~~~~~ + +.. versionadded:: 2.3 + + The ``#[WithStory]`` attribute was added in Foundry 2.3. + +.. warning:: + + The `PHPUnit Extension`_ for Foundry is needed to use ``#[WithStory]`` attribute. + +You can use the ``#[WithStory]`` attribute to load stories in your tests: + +:: + + use App\Story\CategoryStory; + use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; + use Zenstruck\Foundry\Attribute\WithStory; + + // You can use the attribute on the class... + #[WithStory(CategoryStory::class)] + final class NeedsCategoriesTest extends KernelTestCase + { + // ... or on the method + #[WithStory(CategoryStory::class)] + public function testThatNeedStories(): void + { + // ... + } + } + +If used on the class, the story will be loaded before each test method. + Static Analysis --------------- @@ -2080,6 +2280,34 @@ Please, enable it with: $ vendor/bin/psalm-plugin enable zenstruck/foundry +PHPUnit Extension +----------------- + +Foundry is shipped with an extension for PHPUnit. You can install it by modifying the file ``phpunit.xml.dist``: + +.. configuration-block:: + + .. code-block:: xml + + + + + + + +This extension provides the following features: + +* support for the `#[WithStory] Attribute`_ +* ability to use ``Factory::create()`` in `PHPUnit Data Providers`_ (along with PHPUnit ^11.4) + +.. versionadded:: 2.2 + + The PHPUnit extension was introduced in Foundry 2.2. + +.. warning:: + + The PHPUnit extension is only compatible with PHPUnit 10+. + Bundle Configuration -------------------- diff --git a/phpstan.neon b/phpstan.neon index f3e6e4d76..0025a4e03 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,10 +1,23 @@ +includes: + - phar://phpstan.phar/conf/bleedingEdge.neon + parameters: level: 8 + treatPhpDocTypesAsCertain: false + inferPrivatePropertyTypeFromConstructor: true + checkUninitializedProperties: true + checkMissingCallableSignature: true + paths: + - config - src - tests - stubs/phpstan + + banned_code: + non_ignorable: false + ignoreErrors: # suppress strange behavior of PHPStan where it considers proxy() return type as *NEVER* - message: '#Return type of call to function Zenstruck\\Foundry\\Persistence\\proxy contains unresolvable type#' @@ -14,6 +27,47 @@ parameters: - identifier: missingType.iterableValue path: tests/ + # We support both PHPUnit versions (this method changed in PHPUnit 10) + - identifier: function.impossibleType + path: src/Test/Factories.php + + # PHPStan does not understand PHP version checks + - message: '#Comparison operation "(<|>|<=|>=)" between int<80\d+, 80\d+> and 80\d+ is always (false|true).#' + + # Hydrator and Factory are annotated @immutable + - identifier: property.readOnlyByPhpDocDefaultValue + paths: + - src/Object/Hydrator.php + - src/Factory.php + - src/ObjectFactory.php + - src/Persistence/PersistentObjectFactory.php + + # Hydrator and Factory are annotated @immutable + - identifier: property.uninitializedReadonlyByPhpDoc + paths: + - src/Factory.php + - src/Persistence/PersistentObjectFactory.php + + # Hydrator and Factory are annotated @immutable + - identifier: property.readOnlyByPhpDocAssignNotInConstructor + paths: + - src/Object/Hydrator.php + - src/Factory.php + - src/ObjectFactory.php + - src/Persistence/PersistentObjectFactory.php + + # generics annotation are not so helpful in maker code + - identifier: missingType.generics + path: src/Maker/Factory/ + + # not relevant for factories generated by maker + - message: '#Method (.*)::defaults\(\) never returns callable(.*) so it can be removed from the return type.#' + path: tests/Fixture/Maker/expected/ + + # not relevant for factories generated by maker + - identifier: missingType.callable + path: tests/Fixture/Maker/expected/ + excludePaths: - tests/Fixture/Maker/expected/can_create_factory_with_auto_activated_not_persisted_option.php - tests/Fixture/Maker/expected/can_create_factory_interactively.php diff --git a/phpunit b/phpunit index 7de0be16c..dfe4b3f60 100755 --- a/phpunit +++ b/phpunit @@ -8,9 +8,7 @@ check_phpunit_version() { REQUIRED_PHPUNIT_VERSION="${1?}" - if [[ "${INSTALLED_PHPUNIT_VERSION}" == *"dev"* ]] && [ "${REQUIRED_PHPUNIT_VERSION}" != "11.4" ]; then - echo 0; - elif [[ "${INSTALLED_PHPUNIT_VERSION}" == "${REQUIRED_PHPUNIT_VERSION}"* ]]; then + if [[ "${INSTALLED_PHPUNIT_VERSION}" == "${REQUIRED_PHPUNIT_VERSION}"* ]]; then echo 1; else echo 0; @@ -18,7 +16,7 @@ check_phpunit_version() { } ### >> load env vars from .env files if not in CI and not from a composer script -if [ -z "${CI:-}" ] && [ -z "${COMPOSER_BINARY:-}" ] ; then +if [ -z "${CI:-}" ] ; then source .env if [ -f .env.local ]; then @@ -28,8 +26,8 @@ fi ### << ### >> update PHPUnit if needed -if [[ " 9 10 11 11.4 " != *" ${PHPUNIT_VERSION-9} "* ]]; then - echo "❌ PHPUNIT_VERSION should be one of 9, 10, 11, 11.4"; +if [[ " 9 10 11 " != *" ${PHPUNIT_VERSION-9} "* ]]; then + echo "❌ PHPUNIT_VERSION should be one of 9, 10, 11"; exit 1; fi @@ -37,47 +35,48 @@ SHOULD_UPDATE_PHPUNIT=$(check_phpunit_version "${PHPUNIT_VERSION}") if [ "${SHOULD_UPDATE_PHPUNIT}" = "0" ]; then echo "ℹ️ Upgrading PHPUnit to ${PHPUNIT_VERSION}" - if [ "${PHPUNIT_VERSION}" = "9" ]; then - composer update phpunit/phpunit:^9 -W --dev - else - if [ "${PHPUNIT_VERSION}" = "11.4" ]; then - composer update phpunit/phpunit:11.4.x-dev -W --dev - else - composer update "phpunit/phpunit:^${PHPUNIT_VERSION}" -W --dev - fi - fi + composer update brianium/paratest "phpunit/phpunit:^${PHPUNIT_VERSION}" -W fi ### << -### >> guess extensions -EXTENSION="" +### >> actually execute PHPUnit with the right options +DAMA_EXTENSION="DAMA\DoctrineTestBundle\PHPUnit\PHPUnitExtension" +FOUNDRY_EXTENSION="Zenstruck\Foundry\PHPUnit\FoundryExtension" -if [ "${USE_DAMA_DOCTRINE_TEST_BUNDLE:-0}" = "1" ]; then - EXTENSION="DAMA\DoctrineTestBundle\PHPUnit\PHPUnitExtension" +if [ "${USE_FOUNDRY_PHPUNIT_EXTENSION:-0}" = "1" ] && [ "${PHPUNIT_VERSION}" = "9" ]; then + echo "❌ USE_FOUNDRY_PHPUNIT_EXTENSION cannot be used with PHPUNIT_VERSION=10"; + exit 1; fi -### << -### >> actually execute PHPUnit with the right options +PHPUNIT_EXEC="vendor/bin/phpunit" case ${PHPUNIT_VERSION} in "9") - if [ -z "${EXTENSION}" ]; then - vendor/bin/phpunit -c phpunit.xml.dist "$@" - else - vendor/bin/phpunit -c phpunit.xml.dist --extensions "${EXTENSION}" "$@" + PHPUNIT_EXEC="${PHPUNIT_EXEC} -c phpunit.xml.dist" + if [ "${USE_DAMA_DOCTRINE_TEST_BUNDLE:-0}" = "1" ]; then + PHPUNIT_EXEC="${PHPUNIT_EXEC} --extensions ${DAMA_EXTENSION}" fi ;; "10") # PHPUnit 10 does not have a --extension option - vendor/bin/phpunit -c phpunit-10.xml.dist "$@" + PHPUNIT_EXEC="${PHPUNIT_EXEC} -c phpunit-10.xml.dist" ;; - "11"|"11.4") - if [ -z "${EXTENSION}" ]; then - vendor/bin/phpunit -c phpunit-10.xml.dist "$@" - else - vendor/bin/phpunit -c phpunit-10.xml.dist --extension "${EXTENSION}" "$@" + "11") + PHPUNIT_EXEC="${PHPUNIT_EXEC} -c phpunit-10.xml.dist --extension Zenstruck\Foundry\Tests\Fixture\DoctrineCascadeRelationship\PhpUnitTestExtension" + + if [ "${USE_DAMA_DOCTRINE_TEST_BUNDLE:-0}" = "1" ]; then + PHPUNIT_EXEC="${PHPUNIT_EXEC} --extension "${DAMA_EXTENSION}"" + fi + + if [ "${USE_FOUNDRY_PHPUNIT_EXTENSION:-0}" = "1" ]; then + PHPUNIT_EXEC="${PHPUNIT_EXEC} --extension "${FOUNDRY_EXTENSION}"" fi ;; esac + +PHPUNIT_EXEC="${PHPUNIT_EXEC} ${@}" + +echo "${PHPUNIT_EXEC}" +$PHPUNIT_EXEC ### << diff --git a/phpunit-10.xml.dist b/phpunit-10.xml.dist index 1727f9b8b..43f815cc7 100644 --- a/phpunit-10.xml.dist +++ b/phpunit-10.xml.dist @@ -1,12 +1,13 @@ + cacheDirectory=".phpunit.cache" + defaultTestSuite="main"> @@ -14,8 +15,12 @@ - + tests + tests/Integration/ResetDatabase + + + tests/Integration/ResetDatabase diff --git a/phpunit-paratest.xml.dist b/phpunit-paratest.xml.dist new file mode 100644 index 000000000..1f6aee78d --- /dev/null +++ b/phpunit-paratest.xml.dist @@ -0,0 +1,32 @@ + + + + + + + + + + + + tests + tests/Integration/ResetDatabase + + + + + src + + + + + + + + diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 6aee915ff..15de54d9a 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -2,11 +2,12 @@ @@ -16,8 +17,13 @@ - + tests + tests/Integration/ResetDatabase + tests/Integration/ForceFactoriesTraitUsage + + + tests/Integration/ResetDatabase diff --git a/src/Attribute/AsFoundryHook.php b/src/Attribute/AsFoundryHook.php new file mode 100644 index 000000000..68ff14e16 --- /dev/null +++ b/src/Attribute/AsFoundryHook.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\Attribute; + +use Symfony\Component\EventDispatcher\Attribute\AsEventListener; + +#[\Attribute(\Attribute::TARGET_METHOD)] +final class AsFoundryHook extends AsEventListener +{ + public function __construct( + /** @var class-string */ + public readonly ?string $objectClass = null, + int $priority = 0, + ) { + parent::__construct(priority: $priority); + } +} diff --git a/src/Attribute/WithStory.php b/src/Attribute/WithStory.php new file mode 100644 index 000000000..c0b305736 --- /dev/null +++ b/src/Attribute/WithStory.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\Attribute; + +use Zenstruck\Foundry\Story; + +#[\Attribute(\Attribute::IS_REPEATABLE | \Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD)] +final class WithStory +{ + public function __construct( + /** @var class-string $story */ + public readonly string $story, + ) { + if (!\is_subclass_of($story, Story::class)) { + throw new \InvalidArgumentException(\sprintf('"%s" is not a valid story class.', $story)); + } + } +} diff --git a/src/Configuration.php b/src/Configuration.php index c59a4a7ac..dcf436496 100644 --- a/src/Configuration.php +++ b/src/Configuration.php @@ -12,9 +12,13 @@ namespace Zenstruck\Foundry; use Faker; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Zenstruck\Foundry\Exception\FactoriesTraitNotUsed; use Zenstruck\Foundry\Exception\FoundryNotBooted; use Zenstruck\Foundry\Exception\PersistenceDisabled; use Zenstruck\Foundry\Exception\PersistenceNotAvailable; +use Zenstruck\Foundry\InMemory\CannotEnableInMemory; +use Zenstruck\Foundry\InMemory\InMemoryRepositoryRegistry; use Zenstruck\Foundry\Persistence\PersistenceManager; /** @@ -33,18 +37,27 @@ final class Configuration */ public $instantiator; + /** + * This property is only filled if the PHPUnit extension is used! + */ + private bool $bootedForDataProvider = false; + /** @var \Closure():self|self|null */ private static \Closure|self|null $instance = null; + private bool $inMemory = false; + /** * @phpstan-param InstantiatorCallable $instantiator */ public function __construct( - public readonly FactoryRegistry $factories, + public readonly FactoryRegistryInterface $factories, public readonly Faker\Generator $faker, callable $instantiator, public readonly StoryRegistry $stories, private readonly ?PersistenceManager $persistence = null, + private readonly ?EventDispatcherInterface $eventDispatcher = null, + public readonly ?InMemoryRepositoryRegistry $inMemoryRepositoryRegistry = null, ) { $this->instantiator = $instantiator; } @@ -62,19 +75,41 @@ public function isPersistenceAvailable(): bool return (bool) $this->persistence; } - public function assertPersistanceEnabled(): void + public function isPersistenceEnabled(): bool { - if (!$this->isPersistenceAvailable() || !$this->persistence()->isEnabled()) { + return $this->isPersistenceAvailable() && $this->persistence()->isEnabled(); + } + + public function assertPersistenceEnabled(): void + { + if (!$this->isPersistenceEnabled()) { throw new PersistenceDisabled('Cannot get repository when persist is disabled.'); } } + public function hasEventDispatcher(): bool + { + return (bool) $this->eventDispatcher; + } + + public function eventDispatcher(): EventDispatcherInterface + { + return $this->eventDispatcher ?? throw new \RuntimeException('No event dispatcher configured.'); + } + + public function inADataProvider(): bool + { + return $this->bootedForDataProvider; + } + public static function instance(): self { if (!self::$instance) { - throw new FoundryNotBooted('Foundry is not yet booted. Ensure ZenstruckFoundryBundle is enabled. If in a test, ensure your TestCase has the Factories trait.'); + throw new FoundryNotBooted(); } + FactoriesTraitNotUsed::throwIfComingFromKernelTestCaseWithoutFactoriesTrait(); + return \is_callable(self::$instance) ? (self::$instance)() : self::$instance; } @@ -83,14 +118,39 @@ public static function isBooted(): bool return null !== self::$instance; } + /** @param \Closure():self|self $configuration */ public static function boot(\Closure|self $configuration): void { self::$instance = $configuration; } + /** @param \Closure():self|self $configuration */ + public static function bootForDataProvider(\Closure|self $configuration): void + { + self::$instance = \is_callable($configuration) ? ($configuration)() : $configuration; + self::$instance->bootedForDataProvider = true; + } + public static function shutdown(): void { StoryRegistry::reset(); self::$instance = null; } + + /** + * @throws CannotEnableInMemory + */ + public function enableInMemory(): void + { + if (null === $this->inMemoryRepositoryRegistry) { + throw CannotEnableInMemory::noInMemoryRepositoryRegistry(); + } + + $this->inMemory = true; + } + + public function isInMemoryEnabled(): bool + { + return $this->inMemory; + } } diff --git a/src/Exception/CannotCreateFactory.php b/src/Exception/CannotCreateFactory.php new file mode 100644 index 000000000..3845f1310 --- /dev/null +++ b/src/Exception/CannotCreateFactory.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\Exception; + +/** + * @author Nicolas PHILIPPE + * @internal + */ +final class CannotCreateFactory extends \LogicException +{ + public static function argumentCountError(\ArgumentCountError $e): static + { + return new self('Factories with dependencies (services) cannot be created before foundry is booted.', previous: $e); + } +} diff --git a/src/Exception/FactoriesTraitNotUsed.php b/src/Exception/FactoriesTraitNotUsed.php new file mode 100644 index 000000000..810579eb1 --- /dev/null +++ b/src/Exception/FactoriesTraitNotUsed.php @@ -0,0 +1,66 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Exception; + +use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; +use Zenstruck\Foundry\Test\Factories; + +/** + * @author Nicolas PHILIPPE + */ +final class FactoriesTraitNotUsed extends \LogicException +{ + /** + * @param class-string $class + */ + private function __construct(string $class) + { + parent::__construct( + \sprintf('You must use the trait "%s" in "%s" in order to use Foundry.', Factories::class, $class) + ); + } + + public static function throwIfComingFromKernelTestCaseWithoutFactoriesTrait(): void + { + $backTrace = \debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS); // @phpstan-ignore ekinoBannedCode.function + + foreach ($backTrace as $trace) { + if ( + '->' === ($trace['type'] ?? null) + && isset($trace['class']) + && KernelTestCase::class !== $trace['class'] + && \is_a($trace['class'], KernelTestCase::class, allow_string: true) + ) { + self::throwIfClassDoesNotHaveFactoriesTrait($trace['class']); + } + } + } + + /** + * @param class-string $class + */ + public static function throwIfClassDoesNotHaveFactoriesTrait(string $class): void + { + if (!(new \ReflectionClass($class))->hasMethod('_bootFoundry')) { + // throw new self($class); + trigger_deprecation( + 'zenstruck/foundry', + '2.4', + 'In order to use Foundry correctly, you must use the trait "%s" in your "%s" tests. This will throw an exception in 3.0.', + Factories::class, + $class + ); + } + } +} diff --git a/src/Exception/FoundryNotBooted.php b/src/Exception/FoundryNotBooted.php index e4b04e697..5f7fbef27 100644 --- a/src/Exception/FoundryNotBooted.php +++ b/src/Exception/FoundryNotBooted.php @@ -16,4 +16,8 @@ */ final class FoundryNotBooted extends \LogicException { + public function __construct() + { + parent::__construct('Foundry is not yet booted. Ensure ZenstruckFoundryBundle is enabled. If in a test, ensure your TestCase has the Factories trait.'); + } } diff --git a/src/Factory.php b/src/Factory.php index 37d1c84bb..3344e002f 100644 --- a/src/Factory.php +++ b/src/Factory.php @@ -12,6 +12,7 @@ namespace Zenstruck\Foundry; use Faker; +use Zenstruck\Foundry\Exception\CannotCreateFactory; /** * @author Kevin Bond @@ -25,6 +26,13 @@ */ abstract class Factory { + /** + * Memoization of normalized parameters. + * + * @internal + * @var Parameters|null + */ + protected ?array $normalizedParameters = null; /** @phpstan-var Attributes[] */ private array $attributes; @@ -34,7 +42,6 @@ public function __construct() } /** - * @return static * @phpstan-return static * @phpstan-param Attributes $attributes */ @@ -47,10 +54,13 @@ final public static function new(array|callable $attributes = []): static try { $factory ??= new static(); // @phpstan-ignore new.static } catch (\ArgumentCountError $e) { - throw new \LogicException('Factories with dependencies (services) cannot be created before foundry is booted.', previous: $e); + throw CannotCreateFactory::argumentCountError($e); } - return $factory->initialize()->with($attributes); + return $factory + ->initializeInternal() + ->initialize() + ->with($attributes); } /** @@ -101,24 +111,24 @@ final public static function createSequence(iterable|callable $sequence): array abstract public function create(array|callable $attributes = []): mixed; /** - * @return FactoryCollection + * @return FactoryCollection */ final public function many(int $count): FactoryCollection { - return FactoryCollection::many($this, $count); + return FactoryCollection::many($this, $count); // @phpstan-ignore return.type } /** - * @return FactoryCollection + * @return FactoryCollection */ final public function range(int $min, int $max): FactoryCollection { - return FactoryCollection::range($this, $min, $max); + return FactoryCollection::range($this, $min, $max); // @phpstan-ignore return.type } /** * @phpstan-param Sequence $sequence - * @return FactoryCollection + * @return FactoryCollection */ final public function sequence(iterable|callable $sequence): FactoryCollection { @@ -126,7 +136,7 @@ final public function sequence(iterable|callable $sequence): FactoryCollection $sequence = $sequence(); } - return FactoryCollection::sequence($this, $sequence); + return FactoryCollection::sequence($this, $sequence); // @phpstan-ignore return.type } /** @@ -148,6 +158,14 @@ final protected static function faker(): Faker\Generator return Configuration::instance()->faker; } + /** + * Override to adjust default attributes & config. + */ + protected function initialize(): static + { + return $this; + } + /** * @internal * @@ -175,9 +193,9 @@ final protected function normalizeAttributes(array|callable $attributes = []): a } /** - * Override to adjust default attributes & config. + * @internal */ - protected function initialize(): static + protected function initializeInternal(): static { return $this; } @@ -191,9 +209,9 @@ protected function initialize(): static */ protected function normalizeParameters(array $parameters): array { - return array_combine( - array_keys($parameters), - \array_map($this->normalizeParameter(...), array_keys($parameters), $parameters) + return $this->normalizedParameters = \array_combine( + \array_keys($parameters), + \array_map($this->normalizeParameter(...), \array_keys($parameters), $parameters) ); } @@ -202,13 +220,6 @@ protected function normalizeParameters(array $parameters): array */ protected function normalizeParameter(string $field, mixed $value): mixed { - if (\is_array($value)) { - return array_combine( - array_keys($value), - \array_map($this->normalizeParameter(...), array_fill(0, count($value), $field), $value) - ); - } - if ($value instanceof LazyValue) { $value = $value(); } @@ -217,17 +228,28 @@ protected function normalizeParameter(string $field, mixed $value): mixed $value = $value->create(); } + if (FactoryCollection::accepts($value)) { + $value = FactoryCollection::fromFactoriesList($value); + } + if ($value instanceof FactoryCollection) { $value = $this->normalizeCollection($field, $value); } + if (\is_array($value)) { + return \array_combine( + \array_keys($value), + \array_map($this->normalizeParameter(...), \array_fill(0, \count($value), $field), $value) + ); + } + return \is_object($value) ? $this->normalizeObject($value) : $value; } /** * @internal * - * @param FactoryCollection $collection + * @param FactoryCollection> $collection * * @return self[] */ diff --git a/src/FactoryCollection.php b/src/FactoryCollection.php index 55a2e702a..b2319f909 100644 --- a/src/FactoryCollection.php +++ b/src/FactoryCollection.php @@ -11,28 +11,89 @@ namespace Zenstruck\Foundry; +use Zenstruck\Foundry\Persistence\PersistentObjectFactory; +use Zenstruck\Foundry\Persistence\PersistMode; + /** * @author Kevin Bond * * @template T - * @implements \IteratorAggregate> + * @template TFactory of Factory + * @implements \IteratorAggregate * * @phpstan-import-type Attributes from Factory */ final class FactoryCollection implements \IteratorAggregate { + private PersistMode $persistMode; + /** - * @param Factory $factory - * @phpstan-param \Closure():iterable $items + * @param TFactory $factory + * @phpstan-param \Closure():iterable|\Closure():iterable $items */ private function __construct(public readonly Factory $factory, private \Closure $items) { + $this->persistMode = $this->factory instanceof PersistentObjectFactory + ? $this->factory->persistMode() + : PersistMode::WITHOUT_PERSISTING; + } + + /** + * @internal + */ + public function withPersistMode(PersistMode $persistMode): static + { + $clone = clone $this; + $clone->persistMode = $persistMode; + + return $clone; + } + + /** + * @phpstan-assert-if-true non-empty-list $potentialFactories + * + * @internal + */ + public static function accepts(mixed $potentialFactories): bool + { + if (!\is_array($potentialFactories) || 0 === \count($potentialFactories) || !\array_is_list($potentialFactories)) { + return false; + } + + if (!$potentialFactories[0] instanceof ObjectFactory) { + return false; + } + + foreach ($potentialFactories as $potentialFactory) { + if (!$potentialFactory instanceof ObjectFactory + || $potentialFactories[0]::class() !== $potentialFactory::class()) { + return false; + } + } + + return true; + } + + /** + * @param array $factories + * + * @return self + * + * @internal + */ + public static function fromFactoriesList(array $factories): self + { + if (!self::accepts($factories)) { + throw new \InvalidArgumentException('All factories must be of the same type.'); + } + + return new self($factories[0], static fn() => $factories); } /** - * @param Factory $factory + * @param TFactory $factory * - * @return self + * @return self */ public static function many(Factory $factory, int $count): self { @@ -40,9 +101,9 @@ public static function many(Factory $factory, int $count): self } /** - * @param Factory $factory + * @param TFactory $factory * - * @return self + * @return self */ public static function range(Factory $factory, int $min, int $max): self { @@ -54,9 +115,9 @@ public static function range(Factory $factory, int $min, int $max): self } /** - * @param Factory $factory + * @param TFactory $factory * @phpstan-param iterable $items - * @return self + * @return self */ public static function sequence(Factory $factory, iterable $items): self { @@ -74,18 +135,33 @@ public function create(array|callable $attributes = []): array } /** - * @return list> + * @return list */ public function all(): array { $factories = []; $i = 1; - foreach (($this->items)() as $attributes) { - $factories[] = $this->factory->with($attributes)->with(['__index' => $i++]); + foreach (($this->items)() as $attributesOrFactory) { + if ($attributesOrFactory instanceof Factory) { + $factories[] = $attributesOrFactory; + + continue; + } + + $factories[] = $this->factory->with($attributesOrFactory)->with(['__index' => $i++]); } - return $factories; + return \array_map( // @phpstan-ignore return.type (PHPStan does not understand we have an array of factories) + function(Factory $f) { + if ($f instanceof PersistentObjectFactory) { + return $f->withPersistMode($this->persistMode); + } + + return $f; + }, + $factories + ); } public function getIterator(): \Traversable @@ -94,7 +170,7 @@ public function getIterator(): \Traversable } /** - * @return iterable}> + * @return iterable */ public function asDataProvider(): iterable { diff --git a/src/FactoryRegistry.php b/src/FactoryRegistry.php index 2a554e2e5..6c1188248 100644 --- a/src/FactoryRegistry.php +++ b/src/FactoryRegistry.php @@ -11,12 +11,14 @@ namespace Zenstruck\Foundry; +use Zenstruck\Foundry\Exception\CannotCreateFactory; + /** * @author Kevin Bond * * @internal */ -final class FactoryRegistry +final class FactoryRegistry implements FactoryRegistryInterface { /** * @param Factory[] $factories @@ -25,14 +27,7 @@ public function __construct(private iterable $factories) { } - /** - * @template T of Factory - * - * @param class-string $class - * - * @return T|null - */ - public function get(string $class): ?Factory + public function get(string $class): Factory { foreach ($this->factories as $factory) { if ($class === $factory::class) { @@ -40,6 +35,10 @@ public function get(string $class): ?Factory } } - return null; + try { + return new $class(); + } catch (\ArgumentCountError $e) { + throw CannotCreateFactory::argumentCountError($e); + } } } diff --git a/src/FactoryRegistryInterface.php b/src/FactoryRegistryInterface.php new file mode 100644 index 000000000..f2e5a2026 --- /dev/null +++ b/src/FactoryRegistryInterface.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; + +/** + * @author Nicolas PHILIPPE + * + * @internal + */ +interface FactoryRegistryInterface +{ + /** + * @template T of Factory + * + * @param class-string $class + * + * @return T + */ + public function get(string $class): Factory; +} diff --git a/src/InMemory/AsInMemoryTest.php b/src/InMemory/AsInMemoryTest.php new file mode 100644 index 000000000..2f91d551d --- /dev/null +++ b/src/InMemory/AsInMemoryTest.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\InMemory; + +/** + * @author Nicolas PHILIPPE + */ +#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD)] +final class AsInMemoryTest +{ + /** + * @param class-string $class + * @internal + */ + public static function shouldEnableInMemory(string $class, string $method): bool + { + $classReflection = new \ReflectionClass($class); + + if ($classReflection->getAttributes(static::class)) { + return true; + } + + return (bool) $classReflection->getMethod($method)->getAttributes(static::class); + } +} diff --git a/src/InMemory/CannotEnableInMemory.php b/src/InMemory/CannotEnableInMemory.php new file mode 100644 index 000000000..f272b7e09 --- /dev/null +++ b/src/InMemory/CannotEnableInMemory.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\InMemory; + +final class CannotEnableInMemory extends \LogicException +{ + public static function testIsNotAKernelTestCase(string $testName): self + { + return new self("{$testName}: Cannot use the #[AsInMemoryTest] attribute without extending KernelTestCase."); + } + + public static function noInMemoryRepositoryRegistry(): self + { + return new self('Cannot enable "in memory": maybe not in a KernelTestCase?'); + } +} diff --git a/src/InMemory/DependencyInjection/InMemoryCompilerPass.php b/src/InMemory/DependencyInjection/InMemoryCompilerPass.php new file mode 100644 index 000000000..bcdbe99eb --- /dev/null +++ b/src/InMemory/DependencyInjection/InMemoryCompilerPass.php @@ -0,0 +1,55 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\InMemory\DependencyInjection; + +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\Compiler\ServiceLocatorTagPass; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Reference; +use Zenstruck\Foundry\InMemory\InMemoryRepository; + +/** + * @internal + * @author Nicolas PHILIPPE + */ +final class InMemoryCompilerPass implements CompilerPassInterface +{ + public function process(ContainerBuilder $container): void + { + // create a service locator with all "in memory" repositories, indexed by target class + $inMemoryRepositoriesServices = $container->findTaggedServiceIds('foundry.in_memory.repository'); + $inMemoryRepositoriesLocator = ServiceLocatorTagPass::register( + $container, + \array_combine( + \array_map( + static function(string $serviceId) use ($container) { + /** @var class-string> $inMemoryRepositoryClass */ + $inMemoryRepositoryClass = $container->getDefinition($serviceId)->getClass() ?? throw new \LogicException("Service \"{$serviceId}\" should have a class."); + + return $inMemoryRepositoryClass::_class(); + }, + \array_keys($inMemoryRepositoriesServices) + ), + \array_map( + static fn(string $inMemoryRepositoryId) => new Reference($inMemoryRepositoryId), + \array_keys($inMemoryRepositoriesServices) + ), + ) + ); + + $container->findDefinition('.zenstruck_foundry.in_memory.repository_registry') + ->setArgument('$inMemoryRepositories', $inMemoryRepositoriesLocator) + ; + } +} diff --git a/src/InMemory/GenericInMemoryRepository.php b/src/InMemory/GenericInMemoryRepository.php new file mode 100644 index 000000000..9c97c2911 --- /dev/null +++ b/src/InMemory/GenericInMemoryRepository.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\InMemory; + +/** + * @template T of object + * @implements InMemoryRepository + * @author Nicolas PHILIPPE + * + * This class will be used when a specific "in-memory" repository does not exist for a given class. + */ +final class GenericInMemoryRepository implements InMemoryRepository +{ + /** + * @var list + */ + private array $elements = []; + + /** + * @param class-string $class + */ + public function __construct( + private readonly string $class, + ) { + } + + /** + * @param T $item + */ + public function _save(object $item): void + { + if (!$item instanceof $this->class) { + throw new \InvalidArgumentException(\sprintf('Given object of class "%s" is not an instance of expected "%s"', $item::class, $this->class)); + } + + if (!\in_array($item, $this->elements, true)) { + $this->elements[] = $item; + } + } + + public function _all(): array + { + return $this->elements; + } + + public static function _class(): string + { + throw new \BadMethodCallException('This method should not be called on a GenericInMemoryRepository.'); + } +} diff --git a/src/InMemory/InMemoryFactoryRegistry.php b/src/InMemory/InMemoryFactoryRegistry.php new file mode 100644 index 000000000..9a84667a5 --- /dev/null +++ b/src/InMemory/InMemoryFactoryRegistry.php @@ -0,0 +1,59 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\InMemory; + +use Zenstruck\Foundry\Configuration; +use Zenstruck\Foundry\Factory; +use Zenstruck\Foundry\FactoryRegistryInterface; +use Zenstruck\Foundry\ObjectFactory; +use Zenstruck\Foundry\Persistence\PersistentObjectFactory; + +/** + * @internal + * @author Nicolas PHILIPPE + */ +final class InMemoryFactoryRegistry implements FactoryRegistryInterface +{ + public function __construct( + private readonly FactoryRegistryInterface $decorated, + ) { + } + + /** + * @template T of Factory + * + * @param class-string $class + * + * @return T + */ + public function get(string $class): Factory + { + $factory = $this->decorated->get($class); + + if (!$factory instanceof ObjectFactory || !Configuration::instance()->isInMemoryEnabled()) { + return $factory; + } + + if ($factory instanceof PersistentObjectFactory) { + $factory = $factory->withoutPersisting(); + } + + return $factory // @phpstan-ignore argument.templateType + ->afterInstantiate( + function(object $object) use ($factory) { + Configuration::instance()->inMemoryRepositoryRegistry?->get($factory::class())->_save($object); + } + ); + } +} diff --git a/src/InMemory/InMemoryRepository.php b/src/InMemory/InMemoryRepository.php new file mode 100644 index 000000000..e9f4c6936 --- /dev/null +++ b/src/InMemory/InMemoryRepository.php @@ -0,0 +1,37 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\InMemory; + +/** + * @author Nicolas PHILIPPE + * + * @template T of object + */ +interface InMemoryRepository +{ + /** + * @return class-string + */ + public static function _class(): string; + + /** + * @param T $item + */ + public function _save(object $item): void; + + /** + * @return list + */ + public function _all(): array; +} diff --git a/src/InMemory/InMemoryRepositoryRegistry.php b/src/InMemory/InMemoryRepositoryRegistry.php new file mode 100644 index 000000000..982608c27 --- /dev/null +++ b/src/InMemory/InMemoryRepositoryRegistry.php @@ -0,0 +1,50 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\InMemory; + +use Symfony\Component\DependencyInjection\ServiceLocator; + +/** + * @internal + * @author Nicolas PHILIPPE + */ +final class InMemoryRepositoryRegistry +{ + /** + * @var array> + */ + private array $genericInMemoryRepositories = []; + + public function __construct( + /** @var ServiceLocator> */ + private readonly ServiceLocator $inMemoryRepositories, + ) { + } + + /** + * @template T of object + * + * @param class-string $class + * + * @return InMemoryRepository + */ + public function get(string $class): InMemoryRepository + { + if (!$this->inMemoryRepositories->has($class)) { + return $this->genericInMemoryRepositories[$class] ??= new GenericInMemoryRepository($class); // @phpstan-ignore return.type + } + + return $this->inMemoryRepositories->get($class); // @phpstan-ignore return.type + } +} diff --git a/src/InMemory/InMemoryRepositoryTrait.php b/src/InMemory/InMemoryRepositoryTrait.php new file mode 100644 index 000000000..edf1acb16 --- /dev/null +++ b/src/InMemory/InMemoryRepositoryTrait.php @@ -0,0 +1,50 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\InMemory; + +/** + * @template T of object + * @phpstan-require-implements InMemoryRepository + * + * @author Nicolas PHILIPPE + */ +trait InMemoryRepositoryTrait +{ + /** + * @var list + */ + private array $items = []; + + /** + * @param T $item + */ + public function _save(object $item): void + { + if (!\is_a($item, self::_class(), allow_string: true)) { + throw new \InvalidArgumentException(\sprintf('Given object of class "%s" is not an instance of expected "%s"', $item::class, self::_class())); + } + + if (!\in_array($item, $this->items, true)) { + $this->items[] = $item; + } + } + + /** + * @return list + */ + public function _all(): array + { + return $this->items; + } +} diff --git a/src/LazyValue.php b/src/LazyValue.php index b9219714e..53312272e 100644 --- a/src/LazyValue.php +++ b/src/LazyValue.php @@ -18,7 +18,7 @@ final class LazyValue { /** @var \Closure():mixed */ private \Closure $factory; - private mixed $memoizedValue; + private mixed $memoizedValue = null; /** * @param callable():mixed $factory @@ -54,18 +54,24 @@ public function __invoke(): mixed return $value; } + /** + * @param callable():mixed $factory + */ public static function new(callable $factory): self { return new self($factory, false); } + /** + * @param callable():mixed $factory + */ public static function memoize(callable $factory): self { return new self($factory, true); } /** - * @param array $value + * @param array $value * @return array */ private static function normalizeArray(array $value): array diff --git a/src/Maker/Factory/FactoryClassMap.php b/src/Maker/Factory/FactoryClassMap.php index 4a90b6e07..a88b58874 100644 --- a/src/Maker/Factory/FactoryClassMap.php +++ b/src/Maker/Factory/FactoryClassMap.php @@ -25,12 +25,12 @@ final class FactoryClassMap */ private array $classesWithFactories; - /** @param \Traversable $factories */ - public function __construct(\Traversable $factories) // @phpstan-ignore missingType.generics + /** @param \Traversable $factories */ + public function __construct(\Traversable $factories) { $this->classesWithFactories = \array_unique( \array_reduce( - \array_filter(\iterator_to_array($factories, preserve_keys: true), static fn(Factory $f) => $f instanceof ObjectFactory), + \array_filter(\iterator_to_array($factories), static fn(Factory $f) => $f instanceof ObjectFactory), static function(array $carry, ObjectFactory $factory): array { $carry[$factory::class] = $factory::class(); diff --git a/src/Maker/Factory/FactoryGenerator.php b/src/Maker/Factory/FactoryGenerator.php index 7b5560877..f5cfa02cb 100644 --- a/src/Maker/Factory/FactoryGenerator.php +++ b/src/Maker/Factory/FactoryGenerator.php @@ -39,6 +39,7 @@ public function __construct( private \Traversable $defaultPropertiesGuessers, private FactoryClassMap $factoryClassMap, private NamespaceGuesser $namespaceGuesser, + private bool $forceProperties = false, ) { } @@ -108,7 +109,10 @@ function(string $newClassName) use ($factoryClass) { return $factoryClass; } - /** @param class-string $class */ + /** + * @template T of object + * @param class-string $class + */ private function createMakeFactoryData(Generator $generator, string $class, MakeFactoryQuery $makeFactoryQuery): MakeFactoryData { $object = new \ReflectionClass($class); @@ -147,6 +151,7 @@ private function createMakeFactoryData(Generator $generator, string $class, Make $this->staticAnalysisTool(), $persisted ?? false, $makeFactoryQuery->addPhpDoc(), + $this->forceProperties ); } diff --git a/src/Maker/Factory/MakeFactoryData.php b/src/Maker/Factory/MakeFactoryData.php index 9fbac6973..82dce155d 100644 --- a/src/Maker/Factory/MakeFactoryData.php +++ b/src/Maker/Factory/MakeFactoryData.php @@ -15,6 +15,7 @@ use Doctrine\ORM\EntityRepository; use Symfony\Bundle\MakerBundle\Str; use Symfony\Bundle\MakerBundle\Util\ClassNameDetails; +use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor; use Zenstruck\Foundry\ObjectFactory; use Zenstruck\Foundry\Persistence\PersistentProxyObjectFactory; use Zenstruck\Foundry\Persistence\Proxy; @@ -29,6 +30,8 @@ final class MakeFactoryData public const STATIC_ANALYSIS_TOOL_PHPSTAN = 'phpstan'; public const STATIC_ANALYSIS_TOOL_PSALM = 'psalm'; + private static ?ReflectionExtractor $propertyInfo = null; + /** @var list */ private array $uses; /** @var array */ @@ -36,7 +39,6 @@ final class MakeFactoryData /** @var list */ private array $methodsInPHPDoc; - // @phpstan-ignore-next-line public function __construct( private \ReflectionClass $object, private ClassNameDetails $factoryClassNameDetails, @@ -44,6 +46,7 @@ public function __construct( private string $staticAnalysisTool, private bool $persisted, bool $withPhpDoc, + private bool $forceProperties, ) { $this->uses = [ $this->getFactoryClass(), @@ -79,7 +82,7 @@ public function getObjectShortName(): string /** * @return class-string */ - public function getFactoryClass(): string // @phpstan-ignore missingType.generics + public function getFactoryClass(): string { return $this->isPersisted() ? PersistentProxyObjectFactory::class : ObjectFactory::class; } @@ -100,7 +103,7 @@ public function getObjectFullyQualifiedClassName(): string return $this->object->getName(); } - public function getRepositoryReflectionClass(): ?\ReflectionClass // @phpstan-ignore missingType.generics + public function getRepositoryReflectionClass(): ?\ReflectionClass { return $this->repository; } @@ -155,6 +158,22 @@ public function addDefaultProperty(string $propertyName, string $defaultValue): public function getDefaultProperties(): array { $defaultProperties = $this->defaultProperties; + $class = $this->object->getName(); + + /** + * If forceProperties is not set we filter out properties that can not be set because they're either readonly or have no setter. + * Useful for properties that auto generate when the entity is created and can not be changed like a createdAt property for example. + * + * We do this here because we need to get the class of the Entity which only seems to be accessible here. + */ + $defaultProperties = \array_filter($defaultProperties, function(string $propertyName) use ($class): bool { + if (true === $this->forceProperties) { + return true; + } + + return self::propertyInfo()->isWritable($class, $propertyName) || self::propertyInfo()->isInitializable($class, $propertyName); + }, \ARRAY_FILTER_USE_KEY); + \ksort($defaultProperties); return $defaultProperties; @@ -178,7 +197,7 @@ public function addEnumDefaultProperty(string $propertyName, string $enumClass): throw new \LogicException('Cannot add enum for php version inferior than 8.1'); } - if (!enum_exists($enumClass)) { + if (!\enum_exists($enumClass)) { throw new \InvalidArgumentException("Enum of class \"{$enumClass}\" does not exist."); } @@ -190,4 +209,9 @@ public function addEnumDefaultProperty(string $propertyName, string $enumClass): "self::faker()->randomElement({$enumShortClassName}::cases()),", ); } + + private static function propertyInfo(): ReflectionExtractor + { + return self::$propertyInfo ??= new ReflectionExtractor(); + } } diff --git a/src/Maker/Factory/MakeFactoryPHPDocMethod.php b/src/Maker/Factory/MakeFactoryPHPDocMethod.php index b985e2ca2..b6ce55d41 100644 --- a/src/Maker/Factory/MakeFactoryPHPDocMethod.php +++ b/src/Maker/Factory/MakeFactoryPHPDocMethod.php @@ -37,8 +37,8 @@ public static function createAll(MakeFactoryData $makeFactoryData): array if ($makeFactoryData->isPersisted()) { $methods[] = new self($makeFactoryData->getObjectShortName(), 'find(object|array|mixed $criteria)', returnsCollection: false); $methods[] = new self($makeFactoryData->getObjectShortName(), 'findOrCreate(array $attributes)', returnsCollection: false); - $methods[] = new self($makeFactoryData->getObjectShortName(), 'first(string $sortedField = \'id\')', returnsCollection: false); - $methods[] = new self($makeFactoryData->getObjectShortName(), 'last(string $sortedField = \'id\')', returnsCollection: false); + $methods[] = new self($makeFactoryData->getObjectShortName(), 'first(string $sortBy = \'id\')', returnsCollection: false); + $methods[] = new self($makeFactoryData->getObjectShortName(), 'last(string $sortBy = \'id\')', returnsCollection: false); $methods[] = new self($makeFactoryData->getObjectShortName(), 'random(array $attributes = [])', returnsCollection: false); $methods[] = new self($makeFactoryData->getObjectShortName(), 'randomOrCreate(array $attributes = [])', returnsCollection: false); @@ -64,11 +64,10 @@ public function toString(?string $staticAnalysisTool = null): string $returnType = match ((bool) $staticAnalysisTool) { false => "{$this->repository->getShortName()}|ProxyRepositoryDecorator", true => \sprintf( - 'ProxyRepositoryDecorator<%s, %s>', - $this->objectName, + "ProxyRepositoryDecorator<{$this->objectName}, %s>", \is_a($this->repository->getName(), DocumentRepository::class, allow_string: true) - ? 'DocumentRepository' - : 'EntityRepository' + ? "DocumentRepository<{$this->objectName}>" + : "EntityRepository<{$this->objectName}>" ), }; } else { diff --git a/src/Maker/Factory/ObjectDefaultPropertiesGuesser.php b/src/Maker/Factory/ObjectDefaultPropertiesGuesser.php index 5ae259adf..ba6a1378c 100644 --- a/src/Maker/Factory/ObjectDefaultPropertiesGuesser.php +++ b/src/Maker/Factory/ObjectDefaultPropertiesGuesser.php @@ -39,7 +39,7 @@ public function __invoke(SymfonyStyle $io, MakeFactoryData $makeFactoryData, Mak $value = \sprintf('null, // TODO add %svalue manually', $type ? "{$type} " : ''); - if (\PHP_VERSION_ID >= 80100 && enum_exists($type)) { + if (\PHP_VERSION_ID >= 80100 && \enum_exists($type)) { $makeFactoryData->addEnumDefaultProperty($property->getName(), $type); continue; diff --git a/src/Mongo/MongoPersistenceStrategy.php b/src/Mongo/MongoPersistenceStrategy.php index f8dca2c09..f761c5a54 100644 --- a/src/Mongo/MongoPersistenceStrategy.php +++ b/src/Mongo/MongoPersistenceStrategy.php @@ -88,4 +88,11 @@ public function isEmbeddable(object $object): bool { return $this->objectManagerFor($object::class)->getClassMetadata($object::class)->isEmbeddedDocument; } + + public function isScheduledForInsert(object $object): bool + { + $uow = $this->objectManagerFor($object::class)->getUnitOfWork(); + + return $uow->isScheduledForInsert($object) || $uow->isScheduledForUpsert($object); + } } diff --git a/src/Mongo/MongoSchemaResetter.php b/src/Mongo/MongoSchemaResetter.php index 555eb5535..ad40159fc 100644 --- a/src/Mongo/MongoSchemaResetter.php +++ b/src/Mongo/MongoSchemaResetter.php @@ -14,7 +14,9 @@ namespace Zenstruck\Foundry\Mongo; use Symfony\Component\HttpKernel\KernelInterface; -use Zenstruck\Foundry\Persistence\SymfonyCommandRunner; + +use function Zenstruck\Foundry\application; +use function Zenstruck\Foundry\runCommand; /** * @internal @@ -22,8 +24,6 @@ */ final class MongoSchemaResetter implements MongoResetter { - use SymfonyCommandRunner; - /** * @param list $managers */ @@ -33,15 +33,15 @@ public function __construct(private array $managers) public function resetBeforeEachTest(KernelInterface $kernel): void { - $application = self::application($kernel); + $application = application($kernel); foreach ($this->managers as $manager) { try { - self::runCommand($application, 'doctrine:mongodb:schema:drop', ['--dm' => $manager]); + runCommand($application, "doctrine:mongodb:schema:drop --dm={$manager}"); } catch (\Exception) { } - self::runCommand($application, 'doctrine:mongodb:schema:create', ['--dm' => $manager]); + runCommand($application, "doctrine:mongodb:schema:create --dm={$manager}"); } } } diff --git a/src/ORM/AbstractORMPersistenceStrategy.php b/src/ORM/AbstractORMPersistenceStrategy.php index 8fb88cfd5..879902257 100644 --- a/src/ORM/AbstractORMPersistenceStrategy.php +++ b/src/ORM/AbstractORMPersistenceStrategy.php @@ -41,10 +41,13 @@ final public function hasChanges(object $object): bool return false; } + // we're cloning the UOW because computing change set has side effect + $unitOfWork = clone $em->getUnitOfWork(); + // cannot use UOW::recomputeSingleEntityChangeSet() here as it wrongly computes embedded objects as changed - $em->getUnitOfWork()->computeChangeSet($em->getClassMetadata($object::class), $object); + $unitOfWork->computeChangeSet($em->getClassMetadata($object::class), $object); - return (bool) $em->getUnitOfWork()->getEntityChangeSet($object); + return (bool) $unitOfWork->getEntityChangeSet($object); } final public function truncate(string $class): void @@ -78,6 +81,11 @@ final public function isEmbeddable(object $object): bool return $this->objectManagerFor($object::class)->getClassMetadata($object::class)->isEmbeddedClass; } + final public function isScheduledForInsert(object $object): bool + { + return $this->objectManagerFor($object::class)->getUnitOfWork()->isScheduledForInsert($object); + } + final public function managedNamespaces(): array { $namespaces = []; diff --git a/src/ORM/OrmV2PersistenceStrategy.php b/src/ORM/OrmV2PersistenceStrategy.php index 649110c82..7e2ae2a2c 100644 --- a/src/ORM/OrmV2PersistenceStrategy.php +++ b/src/ORM/OrmV2PersistenceStrategy.php @@ -16,7 +16,7 @@ use Doctrine\ORM\Mapping\ClassMetadataInfo; use Doctrine\ORM\Mapping\MappingException as ORMMappingException; use Doctrine\Persistence\Mapping\MappingException; -use Zenstruck\Foundry\Persistence\RelationshipMetadata; +use Zenstruck\Foundry\Persistence\InverseRelationshipMetadata; /** * @internal @@ -25,33 +25,48 @@ */ final class OrmV2PersistenceStrategy extends AbstractORMPersistenceStrategy { - public function relationshipMetadata(string $parent, string $child, string $field): ?RelationshipMetadata + public function inversedRelationshipMetadata(string $parent, string $child, string $field): ?InverseRelationshipMetadata { - $metadata = $this->classMetadata($parent); + $metadata = $this->classMetadata($child); - $association = $this->getAssociationMapping($parent, $field); + $inversedAssociation = $this->getAssociationMapping($parent, $child, $field); - if (null === $association) { - $inversedAssociation = $this->getAssociationMapping($child, $field); + if (null === $inversedAssociation || !$metadata instanceof ClassMetadataInfo) { + return null; + } - if (null === $inversedAssociation || !$metadata instanceof ClassMetadataInfo) { - return null; - } + if (!\is_a( + $child, + $inversedAssociation['targetEntity'], + allow_string: true + )) { // is_a() handles inheritance as well + throw new \LogicException("Cannot find correct association named \"{$field}\" between classes [parent: \"{$parent}\", child: \"{$child}\"]"); + } - if (!\is_a($parent, $inversedAssociation['targetEntity'], allow_string: true)) { // is_a() handles inheritance as well - throw new \LogicException("Cannot find correct association named \"{$field}\" between classes [parent: \"{$parent}\", child: \"{$child}\"]"); - } + // exclude "owning" side of the association (owning OneToOne or ManyToOne) + if (!\in_array( + $inversedAssociation['type'], + [ClassMetadataInfo::ONE_TO_MANY, ClassMetadataInfo::ONE_TO_ONE], + true + ) + || !isset($inversedAssociation['mappedBy']) + ) { + return null; + } - if (ClassMetadataInfo::ONE_TO_MANY !== $inversedAssociation['type'] || !isset($inversedAssociation['mappedBy'])) { - return null; - } + $association = $metadata->getAssociationMapping($inversedAssociation['mappedBy']); - $association = $metadata->getAssociationMapping($inversedAssociation['mappedBy']); + // only keep *ToOne associations + if (!$metadata->isSingleValuedAssociation($association['fieldName'])) { + return null; } - return new RelationshipMetadata( - isCascadePersist: $association['isCascadePersist'], - inverseField: $metadata->isSingleValuedAssociation($association['fieldName']) ? $association['fieldName'] : null, + $inversedAssociationMetadata = $this->classMetadata($inversedAssociation['sourceEntity']); + + return new InverseRelationshipMetadata( + inverseField: $association['fieldName'], + isCollection: $inversedAssociationMetadata->isCollectionValuedAssociation($inversedAssociation['fieldName']), + collectionIndexedBy: $inversedAssociation['indexBy'] ?? null ); } @@ -60,12 +75,18 @@ public function relationshipMetadata(string $parent, string $child, string $fiel * @return array[]|null * @phpstan-return AssociationMapping|null */ - private function getAssociationMapping(string $entityClass, string $field): ?array + private function getAssociationMapping(string $entityClass, string $targetEntity, string $field): ?array { try { - return $this->objectManagerFor($entityClass)->getClassMetadata($entityClass)->getAssociationMapping($field); + $associationMapping = $this->objectManagerFor($entityClass)->getClassMetadata($entityClass)->getAssociationMapping($field); } catch (MappingException|ORMMappingException) { return null; } + + if (!\is_a($targetEntity, $associationMapping['targetEntity'], allow_string: true)) { + return null; + } + + return $associationMapping; } } diff --git a/src/ORM/OrmV3PersistenceStrategy.php b/src/ORM/OrmV3PersistenceStrategy.php index 047208da2..e105b176c 100644 --- a/src/ORM/OrmV3PersistenceStrategy.php +++ b/src/ORM/OrmV3PersistenceStrategy.php @@ -17,50 +17,64 @@ use Doctrine\ORM\Mapping\ClassMetadata; use Doctrine\ORM\Mapping\InverseSideMapping; use Doctrine\ORM\Mapping\MappingException as ORMMappingException; +use Doctrine\ORM\Mapping\ToManyAssociationMapping; use Doctrine\Persistence\Mapping\MappingException; -use Zenstruck\Foundry\Persistence\RelationshipMetadata; +use Zenstruck\Foundry\Persistence\InverseRelationshipMetadata; final class OrmV3PersistenceStrategy extends AbstractORMPersistenceStrategy { - public function relationshipMetadata(string $parent, string $child, string $field): ?RelationshipMetadata + public function inversedRelationshipMetadata(string $parent, string $child, string $field): ?InverseRelationshipMetadata { - $metadata = $this->classMetadata($parent); + $metadata = $this->classMetadata($child); - $association = $this->getAssociationMapping($parent, $field); + $inversedAssociation = $this->getAssociationMapping($parent, $child, $field); - if (null === $association) { - $inversedAssociation = $this->getAssociationMapping($child, $field); + if (null === $inversedAssociation || !$metadata instanceof ClassMetadata) { + return null; + } - if (null === $inversedAssociation || !$metadata instanceof ClassMetadata) { - return null; - } + if (!\is_a( + $child, + $inversedAssociation->targetEntity, + allow_string: true + )) { // is_a() handles inheritance as well + throw new \LogicException("Cannot find correct association named \"{$field}\" between classes [parent: \"{$parent}\", child: \"{$child}\"]"); + } - if (!\is_a($parent, $inversedAssociation->targetEntity, allow_string: true)) { // is_a() handles inheritance as well - throw new \LogicException("Cannot find correct association named \"{$field}\" between classes [parent: \"{$parent}\", child: \"{$child}\"]"); - } + // exclude "owning" side of the association (owning OneToOne or ManyToOne) + if (!$inversedAssociation instanceof InverseSideMapping) { + return null; + } - if (!$inversedAssociation instanceof InverseSideMapping) { - return null; - } + $association = $metadata->getAssociationMapping($inversedAssociation->mappedBy); - $association = $metadata->getAssociationMapping($inversedAssociation->mappedBy); + // only keep *ToOne associations + if (!$metadata->isSingleValuedAssociation($association->fieldName)) { + return null; } - return new RelationshipMetadata( - isCascadePersist: $association->isCascadePersist(), - inverseField: $metadata->isSingleValuedAssociation($association->fieldName) ? $association->fieldName : null, + return new InverseRelationshipMetadata( + inverseField: $association->fieldName, + isCollection: $inversedAssociation instanceof ToManyAssociationMapping, + collectionIndexedBy: $inversedAssociation->isIndexed() ? $inversedAssociation->indexBy() : null ); } /** * @param class-string $entityClass */ - private function getAssociationMapping(string $entityClass, string $field): ?AssociationMapping + private function getAssociationMapping(string $entityClass, string $targetEntity, string $field): ?AssociationMapping { try { - return $this->objectManagerFor($entityClass)->getClassMetadata($entityClass)->getAssociationMapping($field); + $associationMapping = $this->objectManagerFor($entityClass)->getClassMetadata($entityClass)->getAssociationMapping($field); } catch (MappingException|ORMMappingException) { return null; } + + if (!\is_a($targetEntity, $associationMapping->targetEntity, allow_string: true)) { + return null; + } + + return $associationMapping; } } diff --git a/src/ORM/ResetDatabase/BaseOrmResetter.php b/src/ORM/ResetDatabase/BaseOrmResetter.php index 4044fe281..aa15528a1 100644 --- a/src/ORM/ResetDatabase/BaseOrmResetter.php +++ b/src/ORM/ResetDatabase/BaseOrmResetter.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\ORM\ResetDatabase; use Doctrine\Bundle\DoctrineBundle\Registry; @@ -9,15 +18,18 @@ use Doctrine\DBAL\Platforms\PostgreSQLPlatform; use Doctrine\DBAL\Platforms\SQLitePlatform; use Symfony\Bundle\FrameworkBundle\Console\Application; -use Zenstruck\Foundry\Persistence\SymfonyCommandRunner; +use Symfony\Component\Filesystem\Filesystem; +use Symfony\Component\HttpKernel\KernelInterface; + +use function Zenstruck\Foundry\runCommand; /** * @author Nicolas PHILIPPE * @internal */ -abstract class BaseOrmResetter +abstract class BaseOrmResetter implements OrmResetter { - use SymfonyCommandRunner; + private static bool $inFirstTest = true; /** * @param list $managers @@ -30,6 +42,19 @@ public function __construct( ) { } + final public function resetBeforeEachTest(KernelInterface $kernel): void + { + if (self::$inFirstTest) { + self::$inFirstTest = false; + + return; + } + + $this->doResetBeforeEachTest($kernel); + } + + abstract protected function doResetBeforeEachTest(KernelInterface $kernel): void; + final protected function dropAndResetDatabase(Application $application): void { foreach ($this->connections as $connectionName) { @@ -39,29 +64,25 @@ final protected function dropAndResetDatabase(Application $application): void if ($databasePlatform instanceof SQLitePlatform) { // we don't need to create the sqlite database - it's created when the schema is created + // let's only drop the .db file + + $dbPath = $connection->getParams()['path'] ?? null; + if ($dbPath && (new Filesystem())->exists($dbPath)) { + \file_put_contents($dbPath, ''); + } + continue; } if ($databasePlatform instanceof PostgreSQLPlatform) { // let's drop all connections to the database to be able to drop it - self::runCommand( - $application, - 'dbal:run-sql', - [ - '--connection' => $connectionName, - 'sql' => 'SELECT pid, pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = current_database() AND pid <> pg_backend_pid()', - ], - canFail: true, - ); + $sql = 'SELECT pid, pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = current_database() AND pid <> pg_backend_pid()'; + runCommand($application, "dbal:run-sql --connection={$connectionName} '{$sql}'", canFail: true); } - self::runCommand( - $application, - 'doctrine:database:drop', - ['--connection' => $connectionName, '--force' => true, '--if-exists' => true] - ); + runCommand($application, "doctrine:database:drop --connection={$connectionName} --force --if-exists"); - self::runCommand($application, 'doctrine:database:create', ['--connection' => $connectionName]); + runCommand($application, "doctrine:database:create --connection={$connectionName}"); } } } diff --git a/src/ORM/ResetDatabase/MigrateDatabaseResetter.php b/src/ORM/ResetDatabase/MigrateDatabaseResetter.php index fd99b57b2..8e3f321b8 100644 --- a/src/ORM/ResetDatabase/MigrateDatabaseResetter.php +++ b/src/ORM/ResetDatabase/MigrateDatabaseResetter.php @@ -2,16 +2,28 @@ 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\ORM\ResetDatabase; use Doctrine\Bundle\DoctrineBundle\Registry; use Symfony\Component\HttpKernel\KernelInterface; +use function Zenstruck\Foundry\application; +use function Zenstruck\Foundry\runCommand; + /** * @internal * @author Nicolas PHILIPPE */ -final class MigrateDatabaseResetter extends BaseOrmResetter implements OrmResetter +final class MigrateDatabaseResetter extends BaseOrmResetter { /** * @param list $configurations @@ -21,35 +33,34 @@ public function __construct( Registry $registry, array $managers, array $connections, - ) - { + ) { parent::__construct($registry, $managers, $connections); } - final public function resetBeforeEachTest(KernelInterface $kernel): void + public function resetBeforeFirstTest(KernelInterface $kernel): void { $this->resetWithMigration($kernel); } - public function resetBeforeFirstTest(KernelInterface $kernel): void + public function doResetBeforeEachTest(KernelInterface $kernel): void { $this->resetWithMigration($kernel); } private function resetWithMigration(KernelInterface $kernel): void { - $application = self::application($kernel); + $application = application($kernel); $this->dropAndResetDatabase($application); if (!$this->configurations) { - self::runCommand($application, 'doctrine:migrations:migrate'); + runCommand($application, 'doctrine:migrations:migrate'); return; } foreach ($this->configurations as $configuration) { - self::runCommand($application, 'doctrine:migrations:migrate', ['--configuration' => $configuration]); + runCommand($application, "doctrine:migrations:migrate --configuration={$configuration}"); } } } diff --git a/src/ORM/ResetDatabase/ResetDatabaseMode.php b/src/ORM/ResetDatabase/ResetDatabaseMode.php index bd10ef1b1..899789ad4 100644 --- a/src/ORM/ResetDatabase/ResetDatabaseMode.php +++ b/src/ORM/ResetDatabase/ResetDatabaseMode.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\ORM\ResetDatabase; /** diff --git a/src/ORM/ResetDatabase/SchemaDatabaseResetter.php b/src/ORM/ResetDatabase/SchemaDatabaseResetter.php index 40dc765eb..677f13787 100644 --- a/src/ORM/ResetDatabase/SchemaDatabaseResetter.php +++ b/src/ORM/ResetDatabase/SchemaDatabaseResetter.php @@ -16,23 +16,26 @@ use Symfony\Bundle\FrameworkBundle\Console\Application; use Symfony\Component\HttpKernel\KernelInterface; +use function Zenstruck\Foundry\application; +use function Zenstruck\Foundry\runCommand; + /** * @internal * @author Nicolas PHILIPPE */ -final class SchemaDatabaseResetter extends BaseOrmResetter implements OrmResetter +final class SchemaDatabaseResetter extends BaseOrmResetter { public function resetBeforeFirstTest(KernelInterface $kernel): void { - $application = self::application($kernel); + $application = application($kernel); $this->dropAndResetDatabase($application); $this->createSchema($application); } - public function resetBeforeEachTest(KernelInterface $kernel): void + protected function doResetBeforeEachTest(KernelInterface $kernel): void { - $application = self::application($kernel); + $application = application($kernel); $this->dropSchema($application); $this->createSchema($application); @@ -41,22 +44,14 @@ public function resetBeforeEachTest(KernelInterface $kernel): void private function createSchema(Application $application): void { foreach ($this->managers as $manager) { - self::runCommand( - $application, - 'doctrine:schema:update', - ['--em' => $manager, '--force' => true] - ); + runCommand($application, "doctrine:schema:update --em={$manager} --force -v"); } } private function dropSchema(Application $application): void { foreach ($this->managers as $manager) { - self::runCommand( - $application, - 'doctrine:schema:drop', - ['--em' => $manager, '--force' => true, '--full-database' => true] - ); + runCommand($application, "doctrine:schema:drop --em={$manager} --force --full-database"); } } } diff --git a/src/Object/Event/AfterInstantiate.php b/src/Object/Event/AfterInstantiate.php new file mode 100644 index 000000000..356256531 --- /dev/null +++ b/src/Object/Event/AfterInstantiate.php @@ -0,0 +1,43 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Object\Event; + +use Zenstruck\Foundry\Factory; +use Zenstruck\Foundry\ObjectFactory; + +/** + * @author Nicolas PHILIPPE + * + * @template T of object + * @implements Event + * + * @phpstan-import-type Parameters from Factory + */ +final class AfterInstantiate implements Event +{ + public function __construct( + /** @var T */ + public readonly object $object, + /** @phpstan-var Parameters */ + public readonly array $parameters, + /** @var ObjectFactory */ + public readonly ObjectFactory $factory, + ) { + } + + public function objectClassName(): string + { + return $this->object::class; + } +} diff --git a/src/Object/Event/BeforeInstantiate.php b/src/Object/Event/BeforeInstantiate.php new file mode 100644 index 000000000..a79174056 --- /dev/null +++ b/src/Object/Event/BeforeInstantiate.php @@ -0,0 +1,43 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Object\Event; + +use Zenstruck\Foundry\Factory; +use Zenstruck\Foundry\ObjectFactory; + +/** + * @author Nicolas PHILIPPE + * + * @template T of object + * @implements Event + * + * @phpstan-import-type Parameters from Factory + */ +final class BeforeInstantiate implements Event +{ + public function __construct( + /** @phpstan-var Parameters */ + public array $parameters, + /** @var class-string */ + public readonly string $objectClass, + /** @var ObjectFactory */ + public readonly ObjectFactory $factory, + ) { + } + + public function objectClassName(): string + { + return $this->objectClass; + } +} diff --git a/tests/Fixture/Entity/Address/StandardAddress.php b/src/Object/Event/Event.php similarity index 51% rename from tests/Fixture/Entity/Address/StandardAddress.php rename to src/Object/Event/Event.php index 5c64946db..95382cb25 100644 --- a/tests/Fixture/Entity/Address/StandardAddress.php +++ b/src/Object/Event/Event.php @@ -1,5 +1,7 @@ + * @template T of object */ -#[ORM\Entity] -class StandardAddress extends Address +interface Event { + /** + * @return class-string + */ + public function objectClassName(): string; } diff --git a/src/Object/Event/HookListenerFilter.php b/src/Object/Event/HookListenerFilter.php new file mode 100644 index 000000000..12e28e0eb --- /dev/null +++ b/src/Object/Event/HookListenerFilter.php @@ -0,0 +1,45 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Object\Event; + +final class HookListenerFilter +{ + /** @var \Closure(Event): void */ + private \Closure $listener; + + /** + * @param array{0: object, 1: string} $listener + * @param class-string|null $objectClass + */ + public function __construct(array $listener, private ?string $objectClass = null) + { + if (!\is_callable($listener)) { + throw new \InvalidArgumentException(\sprintf('Listener must be a callable, "%s" given.', \get_debug_type($listener))); + } + + $this->listener = $listener(...); + } + + /** + * @param Event $event + */ + public function __invoke(Event $event): void + { + if ($this->objectClass && $event->objectClassName() !== $this->objectClass) { + return; + } + + ($this->listener)($event); + } +} diff --git a/src/Object/Instantiator.php b/src/Object/Instantiator.php index 5d2b71838..9c1b0a4e0 100644 --- a/src/Object/Instantiator.php +++ b/src/Object/Instantiator.php @@ -16,8 +16,6 @@ /** * @author Kevin Bond * - * @immutable - * * @phpstan-import-type Parameters from Factory */ final class Instantiator @@ -28,7 +26,7 @@ final class Instantiator private Hydrator $hydrator; private bool $hydration = true; - private function __construct(private string|\Closure $mode) + private function __construct(private string|\Closure $mode) // @phpstan-ignore missingType.callable { $this->hydrator = new Hydrator(); } @@ -63,7 +61,7 @@ public static function namedConstructor(string $method): self return new self($method); } - public static function use(callable $factory): self + public static function use(callable $factory): self // @phpstan-ignore missingType.callable { return new self($factory(...)); } diff --git a/src/ObjectFactory.php b/src/ObjectFactory.php index 49ef7dc68..4abf7c485 100644 --- a/src/ObjectFactory.php +++ b/src/ObjectFactory.php @@ -11,6 +11,8 @@ namespace Zenstruck\Foundry; +use Zenstruck\Foundry\Object\Event\AfterInstantiate; +use Zenstruck\Foundry\Object\Event\BeforeInstantiate; use Zenstruck\Foundry\Object\Instantiator; /** @@ -24,10 +26,10 @@ */ abstract class ObjectFactory extends Factory { - /** @phpstan-var list):Parameters> */ + /** @phpstan-var list, static):Parameters> */ private array $beforeInstantiate = []; - /** @phpstan-var list */ + /** @phpstan-var list */ private array $afterInstantiate = []; /** @phpstan-var InstantiatorCallable|null */ @@ -46,7 +48,7 @@ public function create(callable|array $attributes = []): object $parameters = $this->normalizeAttributes($attributes); foreach ($this->beforeInstantiate as $hook) { - $parameters = $hook($parameters, static::class()); + $parameters = $hook($parameters, static::class(), $this); if (!\is_array($parameters)) { throw new \LogicException('Before Instantiate hook callback must return a parameter array.'); @@ -59,7 +61,7 @@ public function create(callable|array $attributes = []): object $object = $instantiator($parameters, static::class()); foreach ($this->afterInstantiate as $hook) { - $hook($object, $parameters); + $hook($object, $parameters, $this); } return $object; @@ -80,7 +82,7 @@ final public function instantiateWith(callable $instantiator): static } /** - * @phpstan-param callable(Parameters,class-string):Parameters $callback + * @phpstan-param callable(Parameters, class-string, static):Parameters $callback */ final public function beforeInstantiate(callable $callback): static { @@ -93,7 +95,7 @@ final public function beforeInstantiate(callable $callback): static /** * @final * - * @phpstan-param callable(T,Parameters):void $callback + * @phpstan-param callable(T, Parameters, static):void $callback */ public function afterInstantiate(callable $callback): static { @@ -102,4 +104,31 @@ public function afterInstantiate(callable $callback): static return $clone; } + + /** + * @internal + */ + protected function initializeInternal(): static + { + if (!Configuration::instance()->hasEventDispatcher()) { + return $this; + } + + return $this->beforeInstantiate( + static function(array $parameters, string $objectClass, self $usedFactory): array { + Configuration::instance()->eventDispatcher()->dispatch( + $hook = new BeforeInstantiate($parameters, $objectClass, $usedFactory) + ); + + return $hook->parameters; + } + ) + ->afterInstantiate( + static function(object $object, array $parameters, self $usedFactory): void { + Configuration::instance()->eventDispatcher()->dispatch( + new AfterInstantiate($object, $parameters, $usedFactory) + ); + } + ); + } } diff --git a/src/PHPUnit/BootFoundryOnDataProviderMethodCalled.php b/src/PHPUnit/BootFoundryOnDataProviderMethodCalled.php new file mode 100644 index 000000000..8f58ca6bb --- /dev/null +++ b/src/PHPUnit/BootFoundryOnDataProviderMethodCalled.php @@ -0,0 +1,38 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\PHPUnit; + +use PHPUnit\Event; +use Zenstruck\Foundry\Configuration; +use Zenstruck\Foundry\InMemory\AsInMemoryTest; + +/** + * @internal + * @author Nicolas PHILIPPE + */ +final class BootFoundryOnDataProviderMethodCalled implements Event\Test\DataProviderMethodCalledSubscriber +{ + public function notify(Event\Test\DataProviderMethodCalled $event): void + { + if (\method_exists($event->testMethod()->className(), '_bootForDataProvider')) { + $event->testMethod()->className()::_bootForDataProvider(); + } + + $testMethod = $event->testMethod(); + + if (AsInMemoryTest::shouldEnableInMemory($testMethod->className(), $testMethod->methodName())) { + Configuration::instance()->enableInMemory(); + } + } +} diff --git a/src/PHPUnit/BuildStoryOnTestPrepared.php b/src/PHPUnit/BuildStoryOnTestPrepared.php new file mode 100644 index 000000000..ff3ea9eb4 --- /dev/null +++ b/src/PHPUnit/BuildStoryOnTestPrepared.php @@ -0,0 +1,71 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\PHPUnit; + +use PHPUnit\Event; +use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; +use Zenstruck\Foundry\Attribute\WithStory; +use Zenstruck\Foundry\Exception\FactoriesTraitNotUsed; + +/** + * @internal + * @author Nicolas PHILIPPE + */ +final class BuildStoryOnTestPrepared implements Event\Test\PreparedSubscriber +{ + public function notify(Event\Test\Prepared $event): void + { + $test = $event->test(); + + if (!$test->isTestMethod()) { + return; + } + + /** @var Event\Code\TestMethod $test */ + $reflectionClass = new \ReflectionClass($test->className()); + $withStoryAttributes = [ + ...$this->collectWithStoryAttributesFromClassAndParents($reflectionClass), + ...$reflectionClass->getMethod($test->methodName())->getAttributes(WithStory::class), + ]; + + if (!$withStoryAttributes) { + return; + } + + if (!\is_subclass_of($test->className(), KernelTestCase::class)) { + throw new \InvalidArgumentException(\sprintf('The test class "%s" must extend "%s" to use the "%s" attribute.', $test->className(), KernelTestCase::class, WithStory::class)); + } + + FactoriesTraitNotUsed::throwIfClassDoesNotHaveFactoriesTrait($test->className()); + + foreach ($withStoryAttributes as $withStoryAttribute) { + $withStoryAttribute->newInstance()->story::load(); + } + } + + /** + * @return list<\ReflectionAttribute> + */ + private function collectWithStoryAttributesFromClassAndParents(\ReflectionClass $class): array // @phpstan-ignore missingType.generics + { + return [ + ...$class->getAttributes(WithStory::class), + ...( + $class->getParentClass() + ? $this->collectWithStoryAttributesFromClassAndParents($class->getParentClass()) + : [] + ), + ]; + } +} diff --git a/src/PHPUnit/EnableInMemoryBeforeTest.php b/src/PHPUnit/EnableInMemoryBeforeTest.php new file mode 100644 index 000000000..067b86274 --- /dev/null +++ b/src/PHPUnit/EnableInMemoryBeforeTest.php @@ -0,0 +1,44 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\PHPUnit; + +use PHPUnit\Event; +use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; +use Zenstruck\Foundry\Configuration; +use Zenstruck\Foundry\InMemory\AsInMemoryTest; +use Zenstruck\Foundry\InMemory\CannotEnableInMemory; + +final class EnableInMemoryBeforeTest implements Event\Test\PreparedSubscriber +{ + public function notify(Event\Test\Prepared $event): void + { + $test = $event->test(); + + if (!$test instanceof Event\Code\TestMethod) { + return; + } + + $testClass = $test->className(); + + if (!AsInMemoryTest::shouldEnableInMemory($testClass, $test->methodName())) { + return; + } + + if (!\is_subclass_of($testClass, KernelTestCase::class)) { + throw CannotEnableInMemory::testIsNotAKernelTestCase("{$test->className()}::{$test->methodName()}"); + } + + Configuration::instance()->enableInMemory(); + } +} diff --git a/src/PHPUnit/FoundryExtension.php b/src/PHPUnit/FoundryExtension.php new file mode 100644 index 000000000..851042053 --- /dev/null +++ b/src/PHPUnit/FoundryExtension.php @@ -0,0 +1,47 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\PHPUnit; + +use PHPUnit\Metadata\Version\ConstraintRequirement; +use PHPUnit\Runner; +use PHPUnit\TextUI; +use Zenstruck\Foundry\Configuration; + +/** + * @internal + * @author Nicolas PHILIPPE + */ +final class FoundryExtension implements Runner\Extension\Extension +{ + public function bootstrap( + TextUI\Configuration\Configuration $configuration, + Runner\Extension\Facade $facade, + Runner\Extension\ParameterCollection $parameters, + ): void { + // shutdown Foundry if for some reason it has been booted before + if (Configuration::isBooted()) { + Configuration::shutdown(); + } + + $subscribers = [new BuildStoryOnTestPrepared(), new EnableInMemoryBeforeTest()]; + + if (ConstraintRequirement::from('>=11.4')->isSatisfiedBy(Runner\Version::id())) { + // those deal with data provider events which can be useful only if PHPUnit >=11.4 is used + $subscribers[] = new BootFoundryOnDataProviderMethodCalled(); + $subscribers[] = new ShutdownFoundryOnDataProviderMethodFinished(); + } + + $facade->registerSubscribers(...$subscribers); + } +} diff --git a/src/PHPUnit/ShutdownFoundryOnDataProviderMethodFinished.php b/src/PHPUnit/ShutdownFoundryOnDataProviderMethodFinished.php new file mode 100644 index 000000000..b028394b3 --- /dev/null +++ b/src/PHPUnit/ShutdownFoundryOnDataProviderMethodFinished.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\PHPUnit; + +use PHPUnit\Event; + +/** + * @internal + * @author Nicolas PHILIPPE + */ +final class ShutdownFoundryOnDataProviderMethodFinished implements Event\Test\DataProviderMethodFinishedSubscriber +{ + public function notify(Event\Test\DataProviderMethodFinished $event): void + { + if (\method_exists($event->testMethod()->className(), '_shutdownAfterDataProvider')) { + $event->testMethod()->className()::_shutdownAfterDataProvider(); + } + } +} diff --git a/src/Persistence/Event/AfterPersist.php b/src/Persistence/Event/AfterPersist.php new file mode 100644 index 000000000..6ab3fa293 --- /dev/null +++ b/src/Persistence/Event/AfterPersist.php @@ -0,0 +1,44 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Persistence\Event; + +use Zenstruck\Foundry\Factory; +use Zenstruck\Foundry\Object\Event\Event; +use Zenstruck\Foundry\Persistence\PersistentObjectFactory; + +/** + * @author Nicolas PHILIPPE + * + * @template T of object + * @implements Event + * + * @phpstan-import-type Parameters from Factory + */ +final class AfterPersist implements Event +{ + public function __construct( + /** @var T */ + public readonly object $object, + /** @phpstan-var Parameters */ + public readonly array $parameters, + /** @var PersistentObjectFactory */ + public readonly PersistentObjectFactory $factory, + ) { + } + + public function objectClassName(): string + { + return $this->object::class; + } +} diff --git a/src/Persistence/RelationshipMetadata.php b/src/Persistence/InverseRelationshipMetadata.php similarity index 68% rename from src/Persistence/RelationshipMetadata.php rename to src/Persistence/InverseRelationshipMetadata.php index 6ea69f926..1287b8f56 100644 --- a/src/Persistence/RelationshipMetadata.php +++ b/src/Persistence/InverseRelationshipMetadata.php @@ -16,11 +16,12 @@ * * @internal */ -final class RelationshipMetadata +final class InverseRelationshipMetadata { public function __construct( - public readonly bool $isCascadePersist, - public readonly ?string $inverseField, + public readonly string $inverseField, + public readonly bool $isCollection, + public readonly ?string $collectionIndexedBy, ) { } } diff --git a/src/Persistence/IsProxy.php b/src/Persistence/IsProxy.php index 044af5ac1..0c140b2b4 100644 --- a/src/Persistence/IsProxy.php +++ b/src/Persistence/IsProxy.php @@ -11,7 +11,6 @@ namespace Zenstruck\Foundry\Persistence; -use Doctrine\ODM\MongoDB\DocumentManager; use Symfony\Component\VarExporter\LazyProxyTrait; use Zenstruck\Assert; use Zenstruck\Foundry\Configuration; @@ -26,7 +25,7 @@ * * @mixin LazyProxyTrait */ -trait IsProxy +trait IsProxy // @phpstan-ignore trait.unused { private static array $_autoRefresh = []; @@ -128,19 +127,17 @@ public function _assertNotPersisted(string $message = '{entity} is persisted but return $this; } - private function isPersisted(): bool + public function _initializeLazyObject(): void { - try { - $this->_refresh(); + $this->initializeLazyObject(); + } - return true; - } catch (RefreshObjectFailed $e) { - if ($e->objectWasDeleted()) { - return false; - } + private function isPersisted(): bool + { + $this->initializeLazyObject(); + $object = $this->lazyObjectState->realInstance; - throw $e; - } + return Configuration::instance()->persistence()->isPersisted($object); } private function _autoRefresh(): void diff --git a/src/Persistence/PersistMode.php b/src/Persistence/PersistMode.php new file mode 100644 index 000000000..21ff94a17 --- /dev/null +++ b/src/Persistence/PersistMode.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Persistence; + +/** + * @internal + * @author Nicolas PHILIPPE + */ +enum PersistMode +{ + case PERSIST; + case WITHOUT_PERSISTING; + case NO_PERSIST_BUT_SCHEDULE_FOR_INSERT; + + public function isPersisting(): bool + { + return self::WITHOUT_PERSISTING !== $this; + } +} diff --git a/src/Persistence/PersistenceManager.php b/src/Persistence/PersistenceManager.php index ed18cd056..aaf4d621f 100644 --- a/src/Persistence/PersistenceManager.php +++ b/src/Persistence/PersistenceManager.php @@ -76,7 +76,41 @@ public function save(object $object): object } /** - * @param callable():mixed $callback + * @template T of object + * + * @param T $object + * + * @return T + */ + public function scheduleForInsert(object $object): object + { + if ($object instanceof Proxy) { + $object = unproxy($object); + } + + $om = $this->strategyFor($object::class)->objectManagerFor($object::class); + $om->persist($object); + + return $object; + } + + public function forget(object $object): void + { + if ($this->isPersisted($object)) { + throw new \LogicException('Cannot forget an object already persisted.'); + } + + $om = $this->strategyFor($object::class)->objectManagerFor($object::class); + + $om->detach($object); + } + + /** + * @template T + * + * @param callable():T $callback + * + * @return T */ public function flushAfter(callable $callback): mixed { @@ -145,13 +179,30 @@ public function refresh(object &$object, bool $force = false): object $id = $om->getClassMetadata($object::class)->getIdentifierValues($object); - if (!$id || !$object = $om->find($object::class, $id)) { + if (!$id || !($object = $om->find($object::class, $id))) { // @phpstan-ignore parameterByRef.type throw RefreshObjectFailed::objectNoLongExists(); } return $object; } + public function isPersisted(object $object): bool + { + // prevents doctrine to use its cache and think the object is persisted + if ($this->strategyFor($object::class)->isScheduledForInsert($object)) { + return false; + } + + if ($object instanceof Proxy) { + $object = unproxy($object); + } + + $om = $this->strategyFor($object::class)->objectManagerFor($object::class); + $id = $om->getClassMetadata($object::class)->getIdentifierValues($object); + + return $id && null !== $om->find($object::class, $id); + } + /** * @template T of object * @@ -187,7 +238,11 @@ public function truncate(string $class): void */ public function autoPersist(string $class): bool { - return $this->strategyFor(unproxy($class))->autoPersist(); + try { + return $this->strategyFor(unproxy($class))->autoPersist(); + } catch (NoPersistenceStrategy) { + return false; + } } /** @@ -208,16 +263,19 @@ public function repositoryFor(string $class): ObjectRepository * @param class-string $parent * @param class-string $child */ - public function relationshipMetadata(string $parent, string $child, string $field): ?RelationshipMetadata + public function inverseRelationshipMetadata(string $parent, string $child, string $field): ?InverseRelationshipMetadata { $parent = unproxy($parent); $child = unproxy($child); - return $this->strategyFor($parent)->relationshipMetadata($parent, $child, $field); + return $this->strategyFor($parent)->inversedRelationshipMetadata($parent, $child, $field); } /** - * @param class-string $class + * @template T of object + * + * @param class-string $class + * @return ClassMetadata */ public function metadataFor(string $class): ClassMetadata { @@ -225,7 +283,7 @@ public function metadataFor(string $class): ClassMetadata } /** - * @return iterable + * @return iterable> */ public function allMetadata(): iterable { @@ -269,7 +327,9 @@ public function embeddablePropertiesFor(object $object, string $owner): ?array public function hasPersistenceFor(object $object): bool { try { - return (bool) $this->strategyFor($object::class); + $strategy = $this->strategyFor($object::class); + + return !$strategy->isEmbeddable($object); } catch (NoPersistenceStrategy) { return false; } diff --git a/src/Persistence/PersistenceStrategy.php b/src/Persistence/PersistenceStrategy.php index 975ce2dd9..85607ccde 100644 --- a/src/Persistence/PersistenceStrategy.php +++ b/src/Persistence/PersistenceStrategy.php @@ -63,14 +63,14 @@ public function objectManagers(): array * @param class-string $parent * @param class-string $child */ - public function relationshipMetadata(string $parent, string $child, string $field): ?RelationshipMetadata + public function inversedRelationshipMetadata(string $parent, string $child, string $field): ?InverseRelationshipMetadata { return null; } /** * @template T of object - * @param class-string $class + * @param class-string $class * @return ClassMetadata * * @throws MappingException If $class is not managed by Doctrine @@ -99,4 +99,6 @@ abstract public function managedNamespaces(): array; abstract public function embeddablePropertiesFor(object $object, string $owner): ?array; abstract public function isEmbeddable(object $object): bool; + + abstract public function isScheduledForInsert(object $object): bool; } diff --git a/src/Persistence/PersistentObjectFactory.php b/src/Persistence/PersistentObjectFactory.php index e7ca86489..f118bf4b7 100644 --- a/src/Persistence/PersistentObjectFactory.php +++ b/src/Persistence/PersistentObjectFactory.php @@ -19,9 +19,13 @@ use Zenstruck\Foundry\Factory; use Zenstruck\Foundry\FactoryCollection; use Zenstruck\Foundry\ObjectFactory; +use Zenstruck\Foundry\Persistence\Event\AfterPersist; use Zenstruck\Foundry\Persistence\Exception\NotEnoughObjects; use Zenstruck\Foundry\Persistence\Exception\RefreshObjectFailed; +use function Zenstruck\Foundry\get; +use function Zenstruck\Foundry\set; + /** * @author Kevin Bond * @@ -32,13 +36,13 @@ */ abstract class PersistentObjectFactory extends ObjectFactory { - private bool $persist; + private PersistMode $persist; - /** @phpstan-var list */ + /** @phpstan-var list */ private array $afterPersist = []; /** @var list */ - private array $tempAfterPersist = []; + private array $tempAfterInstantiate = []; /** * @phpstan-param mixed|Parameters $criteriaOrId @@ -164,7 +168,7 @@ public static function all(): array */ public static function repository(): ObjectRepository { - Configuration::instance()->assertPersistanceEnabled(); + Configuration::instance()->assertPersistenceEnabled(); return new RepositoryDecorator(static::class()); // @phpstan-ignore return.type } @@ -194,7 +198,15 @@ public function create(callable|array $attributes = []): object { $object = parent::create($attributes); - if (!$this->isPersisting()) { + foreach ($this->tempAfterInstantiate as $callback) { + $callback($object); + } + + $this->tempAfterInstantiate = []; + + $this->throwIfCannotCreateObject(); + + if (PersistMode::PERSIST !== $this->persistMode()) { return $object; } @@ -206,17 +218,11 @@ public function create(callable|array $attributes = []): object $configuration->persistence()->save($object); - foreach ($this->tempAfterPersist as $callback) { - $callback($object); - } - - $this->tempAfterPersist = []; - if ($this->afterPersist) { - $attributes = $this->normalizeAttributes($attributes); + $attributes = $this->normalizedParameters ?? throw new \LogicException('Factory::$normalizedParameters has not been initialized.'); foreach ($this->afterPersist as $callback) { - $callback($object, $attributes); + $callback($object, $attributes, $this); } $configuration->persistence()->save($object); @@ -228,7 +234,7 @@ public function create(callable|array $attributes = []): object final public function andPersist(): static { $clone = clone $this; - $clone->persist = true; + $clone->persist = PersistMode::PERSIST; return $clone; } @@ -236,13 +242,24 @@ final public function andPersist(): static final public function withoutPersisting(): static { $clone = clone $this; - $clone->persist = false; + $clone->persist = PersistMode::WITHOUT_PERSISTING; return $clone; } /** - * @phpstan-param callable(T, Parameters):void $callback + * @internal + */ + public function withPersistMode(PersistMode $persistMode): static + { + $clone = clone $this; + $clone->persist = $persistMode; + + return $clone; + } + + /** + * @phpstan-param callable(T, Parameters, static):void $callback */ final public function afterPersist(callable $callback): static { @@ -252,6 +269,20 @@ final public function afterPersist(callable $callback): static return $clone; } + /** + * @internal + */ + public function persistMode(): PersistMode + { + $config = Configuration::instance(); + + if (!$config->isPersistenceEnabled() || $config->isInMemoryEnabled()) { + return PersistMode::WITHOUT_PERSISTING; + } + + return $this->persist ?? ($config->persistence()->autoPersist(static::class()) ? PersistMode::PERSIST : PersistMode::WITHOUT_PERSISTING); + } + protected function normalizeParameter(string $field, mixed $value): mixed { if (!Configuration::instance()->isPersistenceAvailable()) { @@ -259,11 +290,37 @@ protected function normalizeParameter(string $field, mixed $value): mixed } if ($value instanceof self && isset($this->persist)) { - $value->persist = $this->persist; // todo - breaks immutability + $value = $value->withPersistMode($this->persist); } - if ($value instanceof self && Configuration::instance()->persistence()->relationshipMetadata(static::class(), $value::class(), $field)?->isCascadePersist) { - $value->persist = false; + if ($value instanceof self) { + $pm = Configuration::instance()->persistence(); + + $inversedRelationshipMetadata = $pm->inverseRelationshipMetadata(static::class(), $value::class(), $field); + + // handle inversed OneToOne + if ($inversedRelationshipMetadata && !$inversedRelationshipMetadata->isCollection) { + $inverseField = $inversedRelationshipMetadata->inverseField; + + // we need to handle the circular dependency involved by inversed one-to-one relationship: + // a placeholder object is used, which will be replaced by the real object, after its instantiation + $inversedObject = $value->withPersistMode(PersistMode::NO_PERSIST_BUT_SCHEDULE_FOR_INSERT) + ->create([$inverseField => $placeholder = (new \ReflectionClass(static::class()))->newInstanceWithoutConstructor()]); + + // auto-refresh computes changeset and prevents the placeholder object to be cleanly + // forgotten fom the persistence manager + if ($inversedObject instanceof Proxy) { + $inversedObject->_disableAutoRefresh(); + $inversedObject = $inversedObject->_real(); + } + + $this->tempAfterInstantiate[] = static function(object $object) use ($inversedObject, $inverseField, $pm, $placeholder) { + $pm->forget($placeholder); + set($inversedObject, $inverseField, $object); + }; + + return $inversedObject; + } } return unproxy(parent::normalizeParameter($field, $value)); @@ -277,10 +334,25 @@ protected function normalizeCollection(string $field, FactoryCollection $collect $pm = Configuration::instance()->persistence(); - if ($inverseField = $pm->relationshipMetadata($collection->factory::class(), static::class(), $field)?->inverseField) { - $this->tempAfterPersist[] = static function(object $object) use ($collection, $inverseField, $pm) { - $collection->create([$inverseField => $object]); - $pm->refresh($object); + $inverseRelationshipMetadata = $pm->inverseRelationshipMetadata(static::class(), $collection->factory::class(), $field); + + if ($inverseRelationshipMetadata && $inverseRelationshipMetadata->isCollection) { + $this->tempAfterInstantiate[] = static function(object $object) use ($collection, $inverseRelationshipMetadata, $field) { + $inverseField = $inverseRelationshipMetadata->inverseField; + + $inverseObjects = $collection->withPersistMode(PersistMode::NO_PERSIST_BUT_SCHEDULE_FOR_INSERT)->create([$inverseField => $object]); + + $inverseObjects = unproxy($inverseObjects); + + // if the collection is indexed by a field, index the array + if ($inverseRelationshipMetadata->collectionIndexedBy) { + $inverseObjects = \array_combine( + \array_map(static fn($o) => get($o, $inverseRelationshipMetadata->collectionIndexedBy), $inverseObjects), + \array_values($inverseObjects) + ); + } + + set($object, $field, $inverseObjects); }; // creation delegated to afterPersist hook - return empty array here @@ -308,12 +380,29 @@ protected function normalizeObject(object $object): object $configuration = Configuration::instance(); - if (!$configuration->isPersistenceAvailable() || !$configuration->persistence()->hasPersistenceFor($object)) { + if (!$configuration->isPersistenceAvailable()) { + return $object; + } + + $persistenceManager = $configuration->persistence(); + + if ($object instanceof Proxy) { + $proxy = $object; + $proxy->_disableAutoRefresh(); + $object = $proxy->_real(); + $proxy->_enableAutoRefresh(); + } + + if (!$persistenceManager->hasPersistenceFor($object)) { return $object; } + if (!$persistenceManager->isPersisted($object)) { + $persistenceManager->scheduleForInsert($object); + } + try { - return proxy($object)->_refresh()->_real(); + return $configuration->persistence()->refresh($object); } catch (RefreshObjectFailed|VarExportLogicException) { return $object; } @@ -323,10 +412,60 @@ final protected function isPersisting(): bool { $config = Configuration::instance(); - if ($config->isPersistenceAvailable() && !$config->persistence()->isEnabled()) { + if ($config->isInMemoryEnabled() || !$config->isPersistenceEnabled()) { return false; } - return $this->persist ?? $config->isPersistenceAvailable() && $config->persistence()->isEnabled() && $config->persistence()->autoPersist(static::class()); + return $this->persistMode()->isPersisting(); + } + + final protected function initializeInternal(): static + { + // Schedule any new object for insert right after instantiation + $factory = parent::initializeInternal() + ->afterInstantiate( + static function(object $object, array $parameters, PersistentObjectFactory $factoryUsed): void { + if (!$factoryUsed->isPersisting()) { + return; + } + + Configuration::instance()->persistence()->scheduleForInsert($object); + } + ); + + if (!Configuration::instance()->hasEventDispatcher()) { + return $factory; + } + + // Dispatch event after persist + return $factory->afterPersist( + static function(object $object, array $parameters, self $factoryUsed): void { + Configuration::instance()->eventDispatcher()->dispatch( + new AfterPersist($object, $parameters, $factoryUsed) + ); + } + ); + } + + private function throwIfCannotCreateObject(): void + { + $configuration = Configuration::instance(); + + /** + * "false === $configuration->inADataProvider()" would also mean that the PHPUnit extension is NOT used + * so a `FoundryNotBooted` exception would be thrown if we actually are in a data provider. + */ + if (!$configuration->inADataProvider()) { + return; + } + + if ( + !$configuration->isPersistenceAvailable() + || $this instanceof PersistentProxyObjectFactory + ) { + return; + } + + throw new \LogicException(\sprintf('Cannot create object in a data provider for non-proxy factories. Transform your factory into a "%s", or call "create()" method in the test. See https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#phpunit-data-providers', PersistentProxyObjectFactory::class)); } } diff --git a/src/Persistence/PersistentProxyObjectFactory.php b/src/Persistence/PersistentProxyObjectFactory.php index a2e78d0a0..a0860a705 100644 --- a/src/Persistence/PersistentProxyObjectFactory.php +++ b/src/Persistence/PersistentProxyObjectFactory.php @@ -31,8 +31,13 @@ abstract public static function class(): string; * @return T|Proxy * @phpstan-return T&Proxy */ - public function create(callable|array $attributes = []): object + final public function create(callable|array $attributes = []): object { + $configuration = Configuration::instance(); + if ($configuration->inADataProvider()) { + return ProxyGenerator::wrapFactory($this, $attributes); + } + return proxy(parent::create($attributes)); // @phpstan-ignore function.unresolvableReturnType } @@ -40,7 +45,7 @@ public function create(callable|array $attributes = []): object * @return T|Proxy * @phpstan-return T&Proxy */ - public static function createOne(array|callable $attributes = []): mixed + final public static function createOne(array|callable $attributes = []): mixed { return proxy(parent::createOne($attributes)); // @phpstan-ignore function.unresolvableReturnType } @@ -136,7 +141,7 @@ final public static function all(): array */ final public static function repository(): ObjectRepository { - Configuration::instance()->assertPersistanceEnabled(); + Configuration::instance()->assertPersistenceEnabled(); return new ProxyRepositoryDecorator(static::class()); // @phpstan-ignore argument.type, return.type } diff --git a/src/Persistence/Proxy.php b/src/Persistence/Proxy.php index eaa49192d..637e0e2ff 100644 --- a/src/Persistence/Proxy.php +++ b/src/Persistence/Proxy.php @@ -87,4 +87,9 @@ public function _assertNotPersisted(string $message = '{entity} is persisted but * @return ProxyRepositoryDecorator> */ public function _repository(): ProxyRepositoryDecorator; + + /** + * @internal + */ + public function _initializeLazyObject(): void; } diff --git a/src/Persistence/ProxyGenerator.php b/src/Persistence/ProxyGenerator.php index 27f5d9d2a..6b26f0a03 100644 --- a/src/Persistence/ProxyGenerator.php +++ b/src/Persistence/ProxyGenerator.php @@ -15,11 +15,14 @@ use Symfony\Component\VarExporter\LazyObjectInterface; use Symfony\Component\VarExporter\LazyProxyTrait; use Symfony\Component\VarExporter\ProxyHelper; +use Zenstruck\Foundry\Factory; /** * @author Kevin Bond * * @internal + * + * @phpstan-import-type Attributes from Factory */ final class ProxyGenerator { @@ -43,6 +46,19 @@ public static function wrap(object $object): Proxy return self::generateClassFor($object)::createLazyProxy(static fn() => $object); // @phpstan-ignore staticMethod.unresolvableReturnType } + /** + * @template T of object + * + * @param PersistentProxyObjectFactory $factory + * @phpstan-param Attributes $attributes + * + * @return T&Proxy + */ + public static function wrapFactory(PersistentProxyObjectFactory $factory, callable|array $attributes): Proxy + { + return self::generateClassFor($factory)::createLazyProxy(static fn() => unproxy($factory->create($attributes))); // @phpstan-ignore-line + } + /** * @template T * @@ -67,6 +83,14 @@ public static function unwrap(mixed $what): mixed return $what; } + /** + * @param class-string $class + */ + public static function proxyClassNameFor(string $class): string + { + return \str_replace('\\', '', $class).'Proxy'; + } + /** * @template T of object * @@ -76,8 +100,8 @@ public static function unwrap(mixed $what): mixed */ private static function generateClassFor(object $object): string { - /** @var class-string $class */ - $class = $object instanceof DoctrineProxy ? \get_parent_class($object) : $object::class; + $class = self::extractClassName($object); + $proxyClass = self::proxyClassNameFor($class); /** @var class-string&T> $proxyClass */ @@ -102,10 +126,14 @@ private static function generateClassFor(object $object): string } /** - * @param class-string $class + * @return class-string */ - public static function proxyClassNameFor(string $class): string + private static function extractClassName(object $object): string { - return \str_replace('\\', '', $class).'Proxy'; + if ($object instanceof PersistentProxyObjectFactory) { + return $object::class(); + } + + return $object instanceof DoctrineProxy ? \get_parent_class($object) : $object::class; // @phpstan-ignore return.type } } diff --git a/src/Persistence/ProxyRepositoryDecorator.php b/src/Persistence/ProxyRepositoryDecorator.php index 666cb98af..578a05c1c 100644 --- a/src/Persistence/ProxyRepositoryDecorator.php +++ b/src/Persistence/ProxyRepositoryDecorator.php @@ -44,9 +44,9 @@ public function firstOrFail(string $sortBy = 'id'): object * @return T|Proxy|null * @psalm-return (T&Proxy)|null */ - public function last(string $sortedField = 'id'): ?object + public function last(string $sortBy = 'id'): ?object { - return $this->proxyNullableObject(parent::last($sortedField)); + return $this->proxyNullableObject(parent::last($sortBy)); } /** @@ -77,7 +77,8 @@ public function findOrFail(mixed $id): object } /** - * @psalm-return array> + * @phpstan-return list> + * @psalm-return list> */ public function findAll(): array { @@ -85,7 +86,7 @@ public function findAll(): array } /** - * @psalm-return array> + * @psalm-return list> */ public function findBy(array $criteria, ?array $orderBy = null, $limit = null, $offset = null): array { @@ -111,7 +112,7 @@ public function random(array $criteria = []): object } /** - * @psalm-return array> + * @psalm-return list> */ public function randomSet(int $count, array $criteria = []): array { @@ -121,7 +122,7 @@ public function randomSet(int $count, array $criteria = []): array } /** - * @psalm-return array> + * @psalm-return list> */ public function randomRange(int $min, int $max, array $criteria = []): array { @@ -148,8 +149,8 @@ public function getClassName(): string } /** - * @param array $objects - * @return array> + * @param list $objects + * @return list> */ private function proxyArray(array $objects): array { diff --git a/src/Persistence/RepositoryDecorator.php b/src/Persistence/RepositoryDecorator.php index a64ca53bf..7dc8af29f 100644 --- a/src/Persistence/RepositoryDecorator.php +++ b/src/Persistence/RepositoryDecorator.php @@ -73,9 +73,9 @@ public function firstOrFail(string $sortBy = 'id'): object /** * @return T|null */ - public function last(string $sortedField = 'id'): ?object + public function last(string $sortBy = 'id'): ?object { - return $this->findBy([], [$sortedField => 'DESC'], 1)[0] ?? null; + return $this->findBy([], [$sortBy => 'DESC'], 1)[0] ?? null; } /** @@ -91,7 +91,7 @@ public function lastOrFail(string $sortBy = 'id'): object */ public function find($id): ?object { - if (\is_array($id) && (empty($id) || !array_is_list($id))) { + if (\is_array($id) && (empty($id) || !\array_is_list($id))) { /** @var T|null $object */ $object = $this->findOneBy($id); @@ -113,22 +113,22 @@ public function findOrFail(mixed $id): object } /** - * @return T[] + * @return list */ public function findAll(): array { - return $this->inner()->findAll(); + return \array_values($this->inner()->findAll()); } /** * @param ?int $limit * @param ?int $offset * - * @return T[] + * @return list */ public function findBy(array $criteria, ?array $orderBy = null, $limit = null, $offset = null): array { - return $this->inner()->findBy($this->normalize($criteria), $orderBy, $limit, $offset); + return \array_values($this->inner()->findBy($this->normalize($criteria), $orderBy, $limit, $offset)); } /** @@ -178,7 +178,7 @@ public function random(array $criteria = []): object * @param positive-int $count * @phpstan-param Parameters $criteria * - * @return T[] + * @return list */ public function randomSet(int $count, array $criteria = []): array { @@ -194,7 +194,7 @@ public function randomSet(int $count, array $criteria = []): array * @param int<0, max> $max * @phpstan-param Parameters $criteria * - * @return T[] + * @return list */ public function randomRange(int $min, int $max, array $criteria = []): array { @@ -229,7 +229,7 @@ public function getIterator(): \Traversable /** * @return ObjectRepository */ - private function inner(): ObjectRepository + public function inner(): ObjectRepository { return Configuration::instance()->persistence()->repositoryFor($this->class); } diff --git a/src/Persistence/ResetDatabase/BeforeEachTestResetter.php b/src/Persistence/ResetDatabase/BeforeEachTestResetter.php index f8ad54bdb..314e16a8c 100644 --- a/src/Persistence/ResetDatabase/BeforeEachTestResetter.php +++ b/src/Persistence/ResetDatabase/BeforeEachTestResetter.php @@ -16,7 +16,6 @@ use Symfony\Component\HttpKernel\KernelInterface; /** - * @internal * @author Nicolas PHILIPPE */ interface BeforeEachTestResetter diff --git a/src/Persistence/ResetDatabase/BeforeFirstTestResetter.php b/src/Persistence/ResetDatabase/BeforeFirstTestResetter.php index 257b15d13..14ef2041c 100644 --- a/src/Persistence/ResetDatabase/BeforeFirstTestResetter.php +++ b/src/Persistence/ResetDatabase/BeforeFirstTestResetter.php @@ -16,7 +16,6 @@ use Symfony\Component\HttpKernel\KernelInterface; /** - * @internal * @author Nicolas PHILIPPE */ interface BeforeFirstTestResetter diff --git a/src/Persistence/SymfonyCommandRunner.php b/src/Persistence/SymfonyCommandRunner.php deleted file mode 100644 index 76c61cf64..000000000 --- a/src/Persistence/SymfonyCommandRunner.php +++ /dev/null @@ -1,49 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Zenstruck\Foundry\Persistence; - -use Symfony\Bundle\FrameworkBundle\Console\Application; -use Symfony\Component\Console\Input\ArrayInput; -use Symfony\Component\Console\Output\BufferedOutput; -use Symfony\Component\HttpKernel\KernelInterface; - -/** - * @internal - * @author Nicolas PHILIPPE - */ -trait SymfonyCommandRunner -{ - /** - * @param array $parameters - */ - final protected static function runCommand(Application $application, string $command, array $parameters = [], bool $canFail = false): void - { - $exit = $application->run( - new ArrayInput(\array_merge(['command' => $command], $parameters + ['--no-interaction' => true])), - $output = new BufferedOutput() - ); - - if (0 !== $exit && !$canFail) { - throw new \RuntimeException(\sprintf('Error running "%s": %s', $command, $output->fetch())); - } - } - - final protected static function application(KernelInterface $kernel): Application - { - $application = new Application($kernel); - $application->setAutoExit(false); - - return $application; - } -} diff --git a/src/Persistence/functions.php b/src/Persistence/functions.php index 99087678b..b38b608c9 100644 --- a/src/Persistence/functions.php +++ b/src/Persistence/functions.php @@ -149,7 +149,11 @@ function delete(object $object): object } /** - * @param callable():mixed $callback + * @template T + * + * @param callable():T $callback + * + * @return T */ function flush_after(callable $callback): mixed { @@ -171,3 +175,15 @@ function enable_persisting(): void { Configuration::instance()->persistence()->enablePersisting(); } + +/** + * @internal + */ +function initialize_proxy_object(mixed $what): void +{ + match (true) { + $what instanceof Proxy => $what->_initializeLazyObject(), + \is_array($what) => \array_map(initialize_proxy_object(...), $what), + default => true, // do nothing + }; +} diff --git a/src/Story.php b/src/Story.php index a0ad80e09..01e407520 100644 --- a/src/Story.php +++ b/src/Story.php @@ -143,7 +143,7 @@ final protected function getState(string $name): mixed $unwrappedObject = ProxyGenerator::unwrap($this->state[$name]); Configuration::instance()->persistence()->refresh($unwrappedObject, force: true); - return $isProxy ? ProxyGenerator::wrap($unwrappedObject) : $unwrappedObject; // @phpstan-ignore argument.templateType + return $isProxy ? ProxyGenerator::wrap($unwrappedObject) : $unwrappedObject; } catch (PersistenceNotAvailable|NoPersistenceStrategy|RefreshObjectFailed) { return $this->state[$name]; } diff --git a/src/Test/Factories.php b/src/Test/Factories.php index e75ac193c..3b7436a2d 100644 --- a/src/Test/Factories.php +++ b/src/Test/Factories.php @@ -16,6 +16,8 @@ use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; use Zenstruck\Foundry\Configuration; +use function Zenstruck\Foundry\Persistence\initialize_proxy_object; + /** * @author Kevin Bond */ @@ -26,32 +28,112 @@ trait Factories * @before */ #[Before] - public static function _bootFoundry(): void + public function _beforeHook(): void { - if (!\is_subclass_of(static::class, KernelTestCase::class)) { // @phpstan-ignore function.impossibleType + $this->_bootFoundry(); + $this->_loadDataProvidedProxies(); + } + + /** + * @internal + * @after + */ + #[After] + public static function _shutdownFoundry(): void + { + Configuration::shutdown(); + } + + /** + * @see \Zenstruck\Foundry\PHPUnit\BootFoundryOnDataProviderMethodCalled + * @internal + */ + public static function _bootForDataProvider(): void + { + if (!\is_subclass_of(static::class, KernelTestCase::class)) { // @phpstan-ignore function.impossibleType, function.alreadyNarrowedType // unit test - Configuration::boot(UnitTestConfig::build()); + Configuration::bootForDataProvider(UnitTestConfig::build()); return; } // integration test - Configuration::boot(static function() { + Configuration::bootForDataProvider(static function(): Configuration { if (!static::getContainer()->has('.zenstruck_foundry.configuration')) { // @phpstan-ignore staticMethod.notFound throw new \LogicException('ZenstruckFoundryBundle is not enabled. Ensure it is added to your config/bundles.php.'); } - return static::getContainer()->get('.zenstruck_foundry.configuration'); // @phpstan-ignore staticMethod.notFound + return static::getContainer()->get('.zenstruck_foundry.configuration'); // @phpstan-ignore staticMethod.notFound, return.type }); } /** * @internal - * @after + * @see \Zenstruck\Foundry\PHPUnit\ShutdownFoundryOnDataProviderMethodFinished */ - #[After] - public static function _shutdownFoundry(): void + public static function _shutdownAfterDataProvider(): void { + if (\is_subclass_of(static::class, KernelTestCase::class)) { // @phpstan-ignore function.impossibleType, function.alreadyNarrowedType + self::ensureKernelShutdown(); // @phpstan-ignore staticMethod.notFound + static::$class = null; // @phpstan-ignore staticProperty.notFound + static::$kernel = null; // @phpstan-ignore staticProperty.notFound + static::$booted = false; // @phpstan-ignore staticProperty.notFound + } Configuration::shutdown(); } + + /** + * @internal + */ + private function _bootFoundry(): void + { + if (!\is_subclass_of(static::class, KernelTestCase::class)) { // @phpstan-ignore function.impossibleType, function.alreadyNarrowedType + // unit test + Configuration::boot(UnitTestConfig::build()); + + return; + } + + // integration test + Configuration::boot(static function(): Configuration { + if (!static::getContainer()->has('.zenstruck_foundry.configuration')) { // @phpstan-ignore staticMethod.notFound + throw new \LogicException('ZenstruckFoundryBundle is not enabled. Ensure it is added to your config/bundles.php.'); + } + + return static::getContainer()->get('.zenstruck_foundry.configuration'); // @phpstan-ignore staticMethod.notFound, return.type + }); + } + + /** + * If a persistent object has been created in a data provider, we need to initialize the proxy object, + * which will trigger the object to be persisted. + * + * Otherwise, such test would not pass: + * ```php + * #[DataProvider('provide')] + * public function testSomething(MyEntity $entity): void + * { + * MyEntityFactory::assert()->count(1); + * } + * + * public static function provide(): iterable + * { + * yield [MyEntityFactory::createOne()]; + * } + * ``` + * + * Sadly, this cannot be done in a subscriber, since PHPUnit does not give access to the actual tests instances. + * + * @internal + */ + private function _loadDataProvidedProxies(): void + { + if (!\is_subclass_of(static::class, KernelTestCase::class)) { // @phpstan-ignore function.impossibleType, function.alreadyNarrowedType + return; + } + + $providedData = \method_exists($this, 'getProvidedData') ? $this->getProvidedData() : $this->providedData(); // @phpstan-ignore method.notFound + + initialize_proxy_object($providedData); + } } diff --git a/src/Test/ResetDatabase.php b/src/Test/ResetDatabase.php index 8b7ee3d79..d17e8bf39 100644 --- a/src/Test/ResetDatabase.php +++ b/src/Test/ResetDatabase.php @@ -16,8 +16,6 @@ use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; use Zenstruck\Foundry\Persistence\ResetDatabase\ResetDatabaseManager; -use function Zenstruck\Foundry\restorePhpUnitErrorHandler; - /** * @author Kevin Bond */ @@ -30,16 +28,13 @@ trait ResetDatabase #[BeforeClass] public static function _resetDatabaseBeforeFirstTest(): void { - if (!\is_subclass_of(static::class, KernelTestCase::class)) { + if (!\is_subclass_of(static::class, KernelTestCase::class)) { // @phpstan-ignore function.alreadyNarrowedType throw new \RuntimeException(\sprintf('The "%s" trait can only be used on TestCases that extend "%s".', __TRAIT__, KernelTestCase::class)); } ResetDatabaseManager::resetBeforeFirstTest( static fn() => static::bootKernel(), - static function(): void { - static::ensureKernelShutdown(); - restorePhpUnitErrorHandler(); - }, + static fn() => static::ensureKernelShutdown(), ); } @@ -50,7 +45,7 @@ static function(): void { #[Before] public static function _resetDatabaseBeforeEachTest(): void { - if (!\is_subclass_of(static::class, KernelTestCase::class)) { + if (!\is_subclass_of(static::class, KernelTestCase::class)) { // @phpstan-ignore function.alreadyNarrowedType throw new \RuntimeException(\sprintf('The "%s" trait can only be used on TestCases that extend "%s".', __TRAIT__, KernelTestCase::class)); } diff --git a/src/ZenstruckFoundryBundle.php b/src/ZenstruckFoundryBundle.php index 21dc84459..db6ef4fb7 100644 --- a/src/ZenstruckFoundryBundle.php +++ b/src/ZenstruckFoundryBundle.php @@ -12,15 +12,25 @@ namespace Zenstruck\Foundry; use Symfony\Component\Config\Definition\Configurator\DefinitionConfigurator; +use Symfony\Component\DependencyInjection\ChildDefinition; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\Exception\LogicException; use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\HttpKernel\Bundle\AbstractBundle; +use Zenstruck\Foundry\Attribute\AsFoundryHook; +use Zenstruck\Foundry\InMemory\DependencyInjection\InMemoryCompilerPass; +use Zenstruck\Foundry\InMemory\InMemoryRepository; use Zenstruck\Foundry\Mongo\MongoResetter; +use Zenstruck\Foundry\Object\Event\Event; +use Zenstruck\Foundry\Object\Event\HookListenerFilter; use Zenstruck\Foundry\Object\Instantiator; +use Zenstruck\Foundry\ORM\ResetDatabase\MigrateDatabaseResetter; use Zenstruck\Foundry\ORM\ResetDatabase\OrmResetter; use Zenstruck\Foundry\ORM\ResetDatabase\ResetDatabaseMode; +use Zenstruck\Foundry\ORM\ResetDatabase\SchemaDatabaseResetter; /** * @author Kevin Bond @@ -229,6 +239,9 @@ public function loadExtension(array $config, ContainerConfigurator $configurator if (!isset($bundles['DoctrineBundle']) && !isset($bundles['DoctrineMongoDBBundle'])) { $container->removeDefinition('.zenstruck_foundry.maker.factory.doctrine_scalar_fields_default_properties_guesser'); } + + $container->getDefinition('.zenstruck_foundry.maker.factory.generator') + ->setArgument('$forceProperties', $config['instantiator']['always_force_properties'] ?? false); } else { $configurator->import('../config/command_stubs.php'); } @@ -249,18 +262,21 @@ public function loadExtension(array $config, ContainerConfigurator $configurator ->replaceArgument('$connections', $config['orm']['reset']['connections']) ; - $container->getDefinition('.zenstruck_foundry.persistence.database_resetter.orm.migrate') - ->replaceArgument('$configurations', $config['orm']['reset']['migrations']['configurations']) - ; - /** @var ResetDatabaseMode $resetMode */ $resetMode = $config['orm']['reset']['mode']; - $toRemove = ResetDatabaseMode::SCHEMA === $resetMode ? ResetDatabaseMode::MIGRATE->value : ResetDatabaseMode::SCHEMA->value; - - $container->removeDefinition(".zenstruck_foundry.persistence.database_resetter.orm.{$toRemove}.dama"); - $container->removeDefinition(".zenstruck_foundry.persistence.database_resetter.orm.{$toRemove}"); - - $container->setAlias(OrmResetter::class, ".zenstruck_foundry.persistence.database_resetter.orm.{$resetMode->value}"); + $container->getDefinition(OrmResetter::class) + ->setClass( + match ($resetMode) { + ResetDatabaseMode::SCHEMA => SchemaDatabaseResetter::class, + ResetDatabaseMode::MIGRATE => MigrateDatabaseResetter::class, + } + ); + + if (ResetDatabaseMode::MIGRATE === $resetMode) { + $container->getDefinition(OrmResetter::class) + ->replaceArgument('$configurations', $config['orm']['reset']['migrations']['configurations']) + ; + } } if (isset($bundles['DoctrineMongoDBBundle'])) { @@ -270,12 +286,33 @@ public function loadExtension(array $config, ContainerConfigurator $configurator ->replaceArgument(1, $config['mongo']) ; - $container->getDefinition('.zenstruck_foundry.persistence.schema_resetter.mongo') + $container->getDefinition(MongoResetter::class) ->replaceArgument(0, $config['mongo']['reset']['document_managers']) ; - - $container->setAlias(MongoResetter::class, '.zenstruck_foundry.persistence.schema_resetter.mongo'); } + + $container->registerAttributeForAutoconfiguration( + AsFoundryHook::class, + // @phpstan-ignore argument.type + static function(ChildDefinition $definition, AsFoundryHook $attribute, \ReflectionMethod $reflector) { + if (1 !== \count($reflector->getParameters()) + || !$reflector->getParameters()[0]->getType() + || !$reflector->getParameters()[0]->getType() instanceof \ReflectionNamedType + || !\is_a($reflector->getParameters()[0]->getType()->getName(), Event::class, true) + ) { + throw new LogicException(\sprintf("In order to use \"%s\" attribute, method \"{$reflector->class}::{$reflector->name}()\" must have a single parameter that is a subclass of \"%s\".", AsFoundryHook::class, Event::class)); + } + $definition->addTag('foundry.hook', [ + 'class' => $attribute->objectClass, + 'method' => $reflector->getName(), + 'event' => $reflector->getParameters()[0]->getType()->getName(), + ]); + } + ); + + $configurator->import('../config/in_memory.php'); + + $container->registerForAutoconfiguration(InMemoryRepository::class)->addTag('foundry.in_memory.repository'); } public function build(ContainerBuilder $container): void @@ -283,6 +320,7 @@ public function build(ContainerBuilder $container): void parent::build($container); $container->addCompilerPass($this); + $container->addCompilerPass(new InMemoryCompilerPass()); } public function process(ContainerBuilder $container): void @@ -294,6 +332,21 @@ public function process(ContainerBuilder $container): void ->addMethodCall('addProvider', [new Reference($id)]) ; } + + // events + $i = 0; + foreach ($container->findTaggedServiceIds('foundry.hook') as $id => $tags) { + foreach ($tags as $tag) { + $container + ->setDefinition("foundry.hook.{$tag['event']}.{$i}", new Definition(class: HookListenerFilter::class)) + ->setArgument(0, [new Reference($id), $tag['method']]) + ->setArgument(1, $tag['class']) + ->addTag('kernel.event_listener', ['event' => $tag['event']]) + ; + + ++$i; + } + } } /** diff --git a/src/functions.php b/src/functions.php index 4b6369d6f..f77cc1154 100644 --- a/src/functions.php +++ b/src/functions.php @@ -68,6 +68,8 @@ function get(object $object, string $property): mixed /** * Create a "lazy" factory attribute which will only be evaluated * if used. + * + * @param callable():mixed $factory */ function lazy(callable $factory): LazyValue { @@ -77,6 +79,8 @@ function lazy(callable $factory): LazyValue /** * Same as {@see lazy()} but subsequent evaluations will return the * same value. + * + * @param callable():mixed $factory */ function memoize(callable $factory): LazyValue { diff --git a/src/phpunit_helper.php b/src/phpunit_helper.php deleted file mode 100644 index 1e5a7a2fe..000000000 --- a/src/phpunit_helper.php +++ /dev/null @@ -1,42 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Zenstruck\Foundry; - -/** - * When using ResetDatabase trait, we're booting the kernel, - * which registers the Symfony's error handler too soon. - * It is then impossible for PHPUnit to handle deprecations. - * - * This method tries to mitigate this problem by restoring the error handler. - * - * @see https://github.com/symfony/symfony/issues/53812 - * - * @internal - */ -function restorePhpUnitErrorHandler(): void -{ - if (!\class_exists(\PHPUnit\Runner\ErrorHandler::class)) { - return; - } - - while (true) { - $previousHandler = \set_error_handler(static fn() => null); // @phpstan-ignore argument.type - \restore_error_handler(); - $isPhpUnitErrorHandler = $previousHandler instanceof \PHPUnit\Runner\ErrorHandler; - if (null === $previousHandler || $isPhpUnitErrorHandler) { - break; - } - \restore_error_handler(); - } -} diff --git a/src/symfony_console.php b/src/symfony_console.php new file mode 100644 index 000000000..d26197f57 --- /dev/null +++ b/src/symfony_console.php @@ -0,0 +1,40 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry; + +use Symfony\Bundle\FrameworkBundle\Console\Application; +use Symfony\Component\Console\Input\StringInput; +use Symfony\Component\Console\Output\BufferedOutput; +use Symfony\Component\HttpKernel\KernelInterface; + +/** + * @internal + */ +function runCommand(Application $application, string $command, bool $canFail = false): void +{ + $exit = $application->run(new StringInput("{$command} --no-interaction"), $output = new BufferedOutput()); + + if (0 !== $exit && !$canFail) { + throw new \RuntimeException(\sprintf('Error running "%s": %s', $command, $output->fetch())); + } +} + +/** + * @internal + */ +function application(KernelInterface $kernel): Application +{ + $application = new Application($kernel); + $application->setAutoExit(false); + + return $application; +} diff --git a/stubs/phpstan/ObjectFactory.php b/stubs/phpstan/ObjectFactory.php index a90cb8e19..5949e9716 100644 --- a/stubs/phpstan/ObjectFactory.php +++ b/stubs/phpstan/ObjectFactory.php @@ -1,5 +1,6 @@ + * @phpstan-import-type Parameters from Factory */ final class UserObjectFactory extends ObjectFactory { @@ -25,7 +27,8 @@ public static function class(): string return UserForObjectFactory::class; } - protected function defaults(): array|callable + /** @return Parameters */ + protected function defaults(): array { return []; } @@ -41,18 +44,20 @@ protected function defaults(): array|callable assertType('UserForObjectFactory', UserObjectFactory::new()->with()->create()); // methods returning a list of objects -assertType("array", UserObjectFactory::createMany(1)); -assertType("array", UserObjectFactory::createRange(1, 2)); -assertType("array", UserObjectFactory::createSequence([])); +assertType("list", UserObjectFactory::createMany(1)); +assertType("list", UserObjectFactory::createRange(1, 2)); +assertType("list", UserObjectFactory::createSequence([])); // methods with FactoryCollection $factoryCollection = FactoryCollection::class; -assertType("{$factoryCollection}", UserObjectFactory::new()->many(2)); -assertType("{$factoryCollection}", UserObjectFactory::new()->range(1, 2)); -assertType("{$factoryCollection}", UserObjectFactory::new()->sequence([])); -assertType("array", UserObjectFactory::new()->many(2)->create()); -assertType("array", UserObjectFactory::new()->range(1, 2)->create()); -assertType("array", UserObjectFactory::new()->sequence([])->create()); +$factory = UserObjectFactory::class; +assertType("{$factoryCollection}", UserObjectFactory::new()->many(2)); +assertType("{$factoryCollection}", UserObjectFactory::new()->range(1, 2)); +assertType("{$factoryCollection}", UserObjectFactory::new()->sequence([])); +assertType("list", UserObjectFactory::new()->many(2)->create()); +assertType("list", UserObjectFactory::new()->range(1, 2)->create()); +assertType("list", UserObjectFactory::new()->sequence([])->create()); +assertType("list<{$factory}>", UserObjectFactory::new()->many(2)->all()); // test autocomplete with phpstorm assertType('string', UserObjectFactory::new()->create()->name); diff --git a/stubs/phpstan/PersistentObjectFactory.php b/stubs/phpstan/PersistentObjectFactory.php index ff946514b..f01610067 100644 --- a/stubs/phpstan/PersistentObjectFactory.php +++ b/stubs/phpstan/PersistentObjectFactory.php @@ -1,5 +1,6 @@ + * @phpstan-import-type Parameters from Factory */ final class UserFactory extends PersistentObjectFactory { @@ -24,7 +26,8 @@ public static function class(): string return UserForPersistentFactory::class; } - protected function defaults(): array|callable + /** @return Parameters */ + protected function defaults(): array { return []; } @@ -43,22 +46,24 @@ protected function defaults(): array|callable assertType('UserForPersistentFactory', UserFactory::new()->with()->create()); // methods returning a list of objects -assertType("array", UserFactory::all()); -assertType("array", UserFactory::createMany(1)); -assertType("array", UserFactory::createRange(1, 2)); -assertType("array", UserFactory::createSequence([])); -assertType("array", UserFactory::randomRange(1, 2)); -assertType("array", UserFactory::randomSet(2)); -assertType("array", UserFactory::findBy(['name' => 'foo'])); +assertType("list", UserFactory::all()); +assertType("list", UserFactory::createMany(1)); +assertType("list", UserFactory::createRange(1, 2)); +assertType("list", UserFactory::createSequence([])); +assertType("list", UserFactory::randomRange(1, 2)); +assertType("list", UserFactory::randomSet(2)); +assertType("list", UserFactory::findBy(['name' => 'foo'])); // methods with FactoryCollection $factoryCollection = FactoryCollection::class; -assertType("{$factoryCollection}", UserFactory::new()->many(2)); -assertType("{$factoryCollection}", UserFactory::new()->range(1, 2)); -assertType("{$factoryCollection}", UserFactory::new()->sequence([])); -assertType("array", UserFactory::new()->many(2)->create()); -assertType("array", UserFactory::new()->range(1, 2)->create()); -assertType("array", UserFactory::new()->sequence([])->create()); +$factory = UserFactory::class; +assertType("{$factoryCollection}", UserFactory::new()->many(2)); +assertType("{$factoryCollection}", UserFactory::new()->range(1, 2)); +assertType("{$factoryCollection}", UserFactory::new()->sequence([])); +assertType("list", UserFactory::new()->many(2)->create()); +assertType("list", UserFactory::new()->range(1, 2)->create()); +assertType("list", UserFactory::new()->sequence([])->create()); +assertType("list<{$factory}>", UserFactory::new()->many(2)->all()); // methods using repository() $repository = UserFactory::repository(); @@ -71,11 +76,11 @@ protected function defaults(): array|callable assertType("UserForPersistentFactory", $repository->findOrFail(1)); assertType("UserForPersistentFactory|null", $repository->findOneBy([])); assertType('UserForPersistentFactory', $repository->random()); -assertType("array", $repository->findAll()); -assertType("array", $repository->findBy([])); -assertType("array", $repository->randomSet(2)); -assertType("array", $repository->randomRange(1, 2)); -assertType('int', $repository->count()); +assertType("list", $repository->findAll()); +assertType("list", $repository->findBy([])); +assertType("list", $repository->randomSet(2)); +assertType("list", $repository->randomRange(1, 2)); +assertType('int<0, max>', $repository->count()); // test autocomplete with phpstorm assertType('string', UserFactory::new()->create()->name); diff --git a/stubs/phpstan/PersistentProxyObjectFactory.php b/stubs/phpstan/PersistentProxyObjectFactory.php index c5768d3d5..1a11f9f5f 100644 --- a/stubs/phpstan/PersistentProxyObjectFactory.php +++ b/stubs/phpstan/PersistentProxyObjectFactory.php @@ -1,5 +1,6 @@ + * @phpstan-import-type Parameters from Factory */ final class UserProxyFactory extends PersistentProxyObjectFactory { @@ -24,7 +26,8 @@ public static function class(): string return UserForProxyFactory::class; } - protected function defaults(): array|callable + /** @return Parameters */ + protected function defaults(): array { return []; } @@ -44,22 +47,24 @@ protected function defaults(): array|callable assertType($proxyType, UserProxyFactory::new()->instantiateWith(Instantiator::withConstructor())->with()->create()); // methods returning a list of objects -assertType("array", UserProxyFactory::all()); -assertType("array", UserProxyFactory::createMany(1)); -assertType("array", UserProxyFactory::createRange(1, 2)); -assertType("array", UserProxyFactory::createSequence([])); -assertType("array", UserProxyFactory::randomRange(1, 2)); -assertType("array", UserProxyFactory::randomSet(2)); -assertType("array", UserProxyFactory::findBy(['name' => 'foo'])); +assertType("list<{$proxyType}>", UserProxyFactory::all()); +assertType("list<{$proxyType}>", UserProxyFactory::createMany(1)); +assertType("list<{$proxyType}>", UserProxyFactory::createRange(1, 2)); +assertType("list<{$proxyType}>", UserProxyFactory::createSequence([])); +assertType("list<{$proxyType}>", UserProxyFactory::randomRange(1, 2)); +assertType("list<{$proxyType}>", UserProxyFactory::randomSet(2)); +assertType("list<{$proxyType}>", UserProxyFactory::findBy(['name' => 'foo'])); // methods with FactoryCollection $factoryCollection = FactoryCollection::class; -assertType("{$factoryCollection}<{$proxyType}>", UserProxyFactory::new()->many(2)); -assertType("{$factoryCollection}<{$proxyType}>", UserProxyFactory::new()->range(1, 2)); -assertType("{$factoryCollection}<{$proxyType}>", UserProxyFactory::new()->sequence([])); -assertType("array", UserProxyFactory::new()->many(2)->create()); -assertType("array", UserProxyFactory::new()->range(1, 2)->create()); -assertType("array", UserProxyFactory::new()->sequence([])->create()); +$factory = UserProxyFactory::class; +assertType("{$factoryCollection}<{$proxyType}, {$factory}>", UserProxyFactory::new()->many(2)); +assertType("{$factoryCollection}<{$proxyType}, {$factory}>", UserProxyFactory::new()->range(1, 2)); +assertType("{$factoryCollection}<{$proxyType}, {$factory}>", UserProxyFactory::new()->sequence([])); +assertType("list<{$proxyType}>", UserProxyFactory::new()->many(2)->create()); +assertType("list<{$proxyType}>", UserProxyFactory::new()->range(1, 2)->create()); +assertType("list<{$proxyType}>", UserProxyFactory::new()->sequence([])->create()); +assertType("list<{$factory}>", UserProxyFactory::new()->many(2)->all()); // methods using repository() $repository = UserProxyFactory::repository(); @@ -72,11 +77,11 @@ protected function defaults(): array|callable assertType($proxyType, $repository->findOrFail(1)); assertType("({$proxyType})|null", $repository->findOneBy([])); assertType($proxyType, $repository->random()); -assertType("array<{$proxyType}>", $repository->findAll()); -assertType("array<{$proxyType}>", $repository->findBy([])); -assertType("array<{$proxyType}>", $repository->randomSet(2)); -assertType("array<{$proxyType}>", $repository->randomRange(1, 2)); -assertType('int', $repository->count()); +assertType("list<{$proxyType}>", $repository->findAll()); +assertType("list<{$proxyType}>", $repository->findBy([])); +assertType("list<{$proxyType}>", $repository->randomSet(2)); +assertType("list<{$proxyType}>", $repository->randomRange(1, 2)); +assertType('int<0, max>', $repository->count()); // check proxy methods assertType($proxyType, UserProxyFactory::new()->create()->_refresh()); diff --git a/stubs/phpstan/functions.php b/stubs/phpstan/functions.php index a57973e41..e06e01b88 100644 --- a/stubs/phpstan/functions.php +++ b/stubs/phpstan/functions.php @@ -10,7 +10,7 @@ class User { - public string $name; + public string $name; // @phpstan-ignore property.uninitialized } assertType('string', factory(UserForPersistentFactory::class)->create()->name); diff --git a/stubs/psalm/ObjectFactory.php b/stubs/psalm/ObjectFactory.php index f51369012..b0a85b97a 100644 --- a/stubs/psalm/ObjectFactory.php +++ b/stubs/psalm/ObjectFactory.php @@ -49,11 +49,11 @@ protected function defaults(): array|callable $var = UserObjectFactory::createSequence([]); // methods with FactoryCollection -/** @psalm-check-type-exact $var = FactoryCollection */ +/** @psalm-check-type-exact $var = FactoryCollection */ $var = UserObjectFactory::new()->many(2); -/** @psalm-check-type-exact $var = FactoryCollection */ +/** @psalm-check-type-exact $var = FactoryCollection */ $var = UserObjectFactory::new()->range(1, 2); -/** @psalm-check-type-exact $var = FactoryCollection */ +/** @psalm-check-type-exact $var = FactoryCollection */ $var = UserObjectFactory::new()->sequence([]); /** @psalm-check-type-exact $var = list */ $var = UserObjectFactory::new()->many(2)->create(); @@ -61,3 +61,5 @@ protected function defaults(): array|callable $var = UserObjectFactory::new()->range(1, 2)->create(); /** @psalm-check-type-exact $var = list */ $var = UserObjectFactory::new()->sequence([])->create(); +/** @psalm-check-type-exact $var = list */ +$var = UserObjectFactory::new()->many(2)->all(); diff --git a/stubs/psalm/PersistentObjectFactory.php b/stubs/psalm/PersistentObjectFactory.php index 8528730ee..0cf71db9c 100644 --- a/stubs/psalm/PersistentObjectFactory.php +++ b/stubs/psalm/PersistentObjectFactory.php @@ -69,11 +69,11 @@ protected function defaults(): array|callable $var = UserFactory::findBy(['name' => 'foo']); // methods with FactoryCollection -/** @psalm-check-type-exact $var = FactoryCollection */ +/** @psalm-check-type-exact $var = FactoryCollection */ $var = UserFactory::new()->many(2); -/** @psalm-check-type-exact $var = FactoryCollection */ +/** @psalm-check-type-exact $var = FactoryCollection */ $var = UserFactory::new()->range(1, 2); -/** @psalm-check-type-exact $var = FactoryCollection */ +/** @psalm-check-type-exact $var = FactoryCollection */ $var = UserFactory::new()->sequence([]); /** @psalm-check-type-exact $var = list */ $var = UserFactory::new()->many(2)->create(); @@ -81,6 +81,8 @@ protected function defaults(): array|callable $var = UserFactory::new()->range(1, 2)->create(); /** @psalm-check-type-exact $var = list */ $var = UserFactory::new()->sequence([])->create(); +/** @psalm-check-type-exact $var = list */ +$var = UserFactory::new()->many(2)->all(); // methods using repository() $repository = UserFactory::repository(); @@ -102,13 +104,13 @@ protected function defaults(): array|callable $var = $repository->findOneBy([]); /** @psalm-check-type-exact $var = UserForPersistentFactory */ $var = $repository->random(); -/** @psalm-check-type-exact $var = array */ +/** @psalm-check-type-exact $var = list */ $var = $repository->findAll(); -/** @psalm-check-type-exact $var = array */ +/** @psalm-check-type-exact $var = list */ $var = $repository->findBy([]); -/** @psalm-check-type-exact $var = array */ +/** @psalm-check-type-exact $var = list */ $var = $repository->randomSet(2); -/** @psalm-check-type-exact $var = array */ +/** @psalm-check-type-exact $var = list */ $var = $repository->randomRange(1, 2); /** @psalm-check-type-exact $var = int */ $var = $repository->count(); diff --git a/stubs/psalm/PersistentProxyObjectFactory.php b/stubs/psalm/PersistentProxyObjectFactory.php index 10a8f4e5f..9525a31de 100644 --- a/stubs/psalm/PersistentProxyObjectFactory.php +++ b/stubs/psalm/PersistentProxyObjectFactory.php @@ -69,11 +69,11 @@ protected function defaults(): array|callable $var = UserProxyFactory::findBy(['name' => 'foo']); // methods with FactoryCollection -/** @psalm-check-type-exact $var = FactoryCollection> */ +/** @psalm-check-type-exact $var = FactoryCollection, UserProxyFactory> */ $var = UserProxyFactory::new()->many(2); -/** @psalm-check-type-exact $var = FactoryCollection> */ +/** @psalm-check-type-exact $var = FactoryCollection, UserProxyFactory> */ $var = UserProxyFactory::new()->range(1, 2); -/** @psalm-check-type-exact $var = FactoryCollection> */ +/** @psalm-check-type-exact $var = FactoryCollection, UserProxyFactory> */ $var = UserProxyFactory::new()->sequence([]); /** @psalm-check-type-exact $var = list> */ $var = UserProxyFactory::new()->many(2)->create(); @@ -81,6 +81,9 @@ protected function defaults(): array|callable $var = UserProxyFactory::new()->range(1, 2)->create(); /** @psalm-check-type-exact $var = list> */ $var = UserProxyFactory::new()->sequence([])->create(); +// not working... ? +///** @psalm-check-type-exact $var = list */ +//$var = UserProxyFactory::new()->many(2)->all(); // methods using repository() $repository = UserProxyFactory::repository(); @@ -102,13 +105,13 @@ protected function defaults(): array|callable $var = $repository->findOneBy([]); /** @psalm-check-type-exact $var = UserForProxyFactory&Proxy */ $var = $repository->random(); -/** @psalm-check-type-exact $var = array> */ +/** @psalm-check-type-exact $var = list> */ $var = $repository->findAll(); -/** @psalm-check-type-exact $var = array> */ +/** @psalm-check-type-exact $var = list> */ $var = $repository->findBy([]); -/** @psalm-check-type-exact $var = array> */ +/** @psalm-check-type-exact $var = list> */ $var = $repository->randomSet(2); -/** @psalm-check-type-exact $var = array> */ +/** @psalm-check-type-exact $var = list> */ $var = $repository->randomRange(1, 2); /** @psalm-check-type-exact $var = int */ $var = $repository->count(); diff --git a/tests/Fixture/CustomMigrations/Version20240611065130.php b/tests/Fixture/CustomMigrations/Version20240611065130.php deleted file mode 100644 index 9d51237ff..000000000 --- a/tests/Fixture/CustomMigrations/Version20240611065130.php +++ /dev/null @@ -1,44 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -// to "Migrations" directory on boot (cf. bootstrap.php) - -namespace Zenstruck\Foundry\Tests\Fixture\Migrations; - -use Doctrine\DBAL\Schema\Schema; -use Doctrine\Migrations\AbstractMigration; -use Zenstruck\Foundry\Tests\Fixture\EdgeCases\Migrate\ORM\EntityInAnotherSchema\Article; - -/** - * Create custom "cms" schema ({@see Article}) to ensure "migrate" mode is still working with multiple schemas. - * Note: the doctrine:migrations:diff command doesn't seem able to add this custom "CREATE SCHEMA" automatically. - * - * @see https://github.com/zenstruck/foundry/issues/618 - */ -final class Version20240611065130 extends AbstractMigration -{ - public function getDescription(): string - { - return 'Create custom "cms" schema.'; - } - - public function up(Schema $schema): void - { - $this->addSql('CREATE SCHEMA cms'); - } - - public function down(Schema $schema): void - { - $this->addSql('DROP SCHEMA cms'); - } -} diff --git a/tests/Fixture/DoctrineCascadeRelationship/ChangeCascadePersistOnLoadClassMetadataListener.php b/tests/Fixture/DoctrineCascadeRelationship/ChangeCascadePersistOnLoadClassMetadataListener.php new file mode 100644 index 000000000..ba3327ef9 --- /dev/null +++ b/tests/Fixture/DoctrineCascadeRelationship/ChangeCascadePersistOnLoadClassMetadataListener.php @@ -0,0 +1,54 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Tests\Fixture\DoctrineCascadeRelationship; + +use Doctrine\Bundle\DoctrineBundle\Attribute\AsDoctrineListener; +use Doctrine\ORM\Event\LoadClassMetadataEventArgs; +use Doctrine\ORM\Events; + +/** + * @author Nicolas PHILIPPE + * + * This class changes the "cascade persist" value of a doctrine relationship. + * @see ChangesEntityRelationshipCascadePersist + */ +#[AsDoctrineListener(event: Events::loadClassMetadata)] +final class ChangeCascadePersistOnLoadClassMetadataListener +{ + /** @var list */ + private array $metadata = []; + + /** + * @param list $metadata + */ + public function withMetadata(array $metadata): void + { + $this->metadata = $metadata; + } + + public function loadClassMetadata(LoadClassMetadataEventArgs $eventArgs): void + { + $classMetadata = $eventArgs->getClassMetadata(); + + foreach ($this->metadata as $metadatum) { + if ($classMetadata->getName() === $metadatum->class) { + $classMetadata->getAssociationMapping($metadatum->field)['cascade'] = $metadatum->cascade ? ['persist'] : []; + + if ($metadatum->orphanRemoval) { + $classMetadata->getAssociationMapping($metadatum->field)['orphanRemoval'] = true; + } + } + } + } +} diff --git a/tests/Fixture/DoctrineCascadeRelationship/ChangesEntityRelationshipCascadePersist.php b/tests/Fixture/DoctrineCascadeRelationship/ChangesEntityRelationshipCascadePersist.php new file mode 100644 index 000000000..afeec36bb --- /dev/null +++ b/tests/Fixture/DoctrineCascadeRelationship/ChangesEntityRelationshipCascadePersist.php @@ -0,0 +1,132 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Tests\Fixture\DoctrineCascadeRelationship; + +use Doctrine\ORM\Mapping\ClassMetadata; +use Doctrine\ORM\Mapping\MappingException; +use PHPUnit\Framework\Attributes\Before; +use PHPUnit\Framework\Attributes\DataProvider; +use Psr\Cache\CacheItemPoolInterface; +use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; +use Zenstruck\Foundry\Persistence\PersistenceManager; +use Zenstruck\Foundry\Tests\Integration\RequiresORM; + +/** + * @author Nicolas PHILIPPE + * + * Hack into PHPUnit data provider mechanism to change the cascade persist behavior of Doctrine relationship: + * - each test declares which relationship it uses, thanks to UsingRelationships attribute + * - the data provider provides all possible combinations of relationships to change the cascade persist behavior + * - the listener {@see ChangeCascadePersistOnLoadClassMetadataListener} is used to change the cascade persist behavior + * before each test, thanks to "onLoadMetadata" doctrine event. + * + * This way, we can test all possible combinations of "cascade persist" on doctrine relationships. + */ +trait ChangesEntityRelationshipCascadePersist +{ + use RequiresORM; + + private static string $methodName = ''; + + #[Before] + public function setUpCascadePersistMetadata(): void + { + if (!$this instanceof KernelTestCase) { + throw new \LogicException('Cannot use trait "ChangesEntityRelationshipCascadePersist" without KernelTestCase.'); + } + + $testMethod = new \ReflectionMethod(static::class, $this->name()); + $usingRelationshipsAttributes = $testMethod->getAttributes(UsingRelationships::class); + + if (!$usingRelationshipsAttributes) { + return; + } + + $usingRelationshipsAttributes = $testMethod->getAttributes(DataProvider::class); + if (1 !== \count($usingRelationshipsAttributes) || 'provideCascadeRelationshipsCombinations' !== $usingRelationshipsAttributes[0]->newInstance()->methodName()) { + throw new \LogicException(\sprintf('When using attribute "%s", you must use "provideCascadeRelationshipsCombinations" as unique a data provider.', UsingRelationships::class)); + } + + /** @var ChangeCascadePersistOnLoadClassMetadataListener $changeCascadePersistListener */ + $changeCascadePersistListener = self::getContainer()->get(ChangeCascadePersistOnLoadClassMetadataListener::class); + $changeCascadePersistListener->withMetadata(\array_values($this->providedData())); + + /** @var CacheItemPoolInterface $doctrineMetadataCache */ + $doctrineMetadataCache = self::getContainer()->get('doctrine.orm.default_metadata_cache'); + $doctrineMetadataCache->clear(); + } + + /** + * @return iterable> + */ + public static function provideCascadeRelationshipsCombinations(): iterable + { + if (!\getenv('DATABASE_URL')) { + // this test requires the ORM, but trait RequiresORM is analysed after data provider are called + // then we need to return at least one empty array to avoid an error + // in PHPUnit 12, we will be able to use #[RequiresEnvironmentVariable('DATABASE_URL')] to prevent this + yield ['']; // @phpstan-ignore generator.valueType + + return; + } + + /** + * self::$methodName is set in a PHPUnit extension, it's the only way to get the current method name. + * @see PhpUnitTestExtension + */ + $attributes = (new \ReflectionMethod(static::class, self::$methodName))->getAttributes(UsingRelationships::class); + + $relationshipsToChange = []; + foreach ($attributes as $attribute) { + /** @var UsingRelationships $attributeInstance */ + $attributeInstance = $attribute->newInstance(); + $relationshipsToChange[$attributeInstance->class] = $attributeInstance->relationShips; + } + + /** @var PersistenceManager $persistenceManager */ + $persistenceManager = self::getContainer()->get(PersistenceManager::class); + + $relationshipFields = []; + foreach ($relationshipsToChange as $class => $fields) { + $metadata = $persistenceManager->metadataFor($class); + + if (!$metadata instanceof ClassMetadata || $metadata->isEmbeddedClass) { + throw new \InvalidArgumentException("{$class} is not an entity using ORM"); + } + + foreach ($fields as $field) { + try { + $association = $metadata->getAssociationMapping($field); + } catch (MappingException) { + throw new \LogicException(\sprintf("Wrong parameters for attribute \"%s\". Association \"{$class}::\${$field}\" does not exist.", UsingRelationships::class)); + } + + $relationshipFields[] = ['class' => $association['sourceEntity'], 'field' => $association['fieldName'], 'isOneToMany' => ClassMetadata::ONE_TO_MANY === $association['type']]; + if ($association['inversedBy'] ?? $association['mappedBy'] ?? null) { + /** @var ClassMetadata $metadataTargetEntity */ + $metadataTargetEntity = $persistenceManager->metadataFor($association['targetEntity']); // @phpstan-ignore argument.templateType + $associationTargetEntity = $metadataTargetEntity->getAssociationMapping($association['inversedBy'] ?? $association['mappedBy']); + $relationshipFields[] = ['class' => $associationTargetEntity['sourceEntity'], 'field' => $associationTargetEntity['fieldName'], 'isOneToMany' => ClassMetadata::ONE_TO_MANY === $associationTargetEntity['type']]; + } + } + } + + yield from DoctrineCascadeRelationshipMetadata::allCombinations($relationshipFields); + } + + public static function setCurrentProvidedMethodName(string $methodName): void + { + self::$methodName = $methodName; + } +} diff --git a/tests/Fixture/DoctrineCascadeRelationship/DoctrineCascadeRelationshipMetadata.php b/tests/Fixture/DoctrineCascadeRelationship/DoctrineCascadeRelationshipMetadata.php new file mode 100644 index 000000000..a5b3b54ed --- /dev/null +++ b/tests/Fixture/DoctrineCascadeRelationship/DoctrineCascadeRelationshipMetadata.php @@ -0,0 +1,100 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Tests\Fixture\DoctrineCascadeRelationship; + +/** + * @author Nicolas PHILIPPE + */ +final class DoctrineCascadeRelationshipMetadata implements \Stringable +{ + private function __construct( + public readonly string $class, + public readonly string $field, + public readonly bool $cascade, + public readonly bool $orphanRemoval, + ) { + } + + public function __toString(): string + { + $name = \sprintf('%s::$%s - %s', $this->class, $this->field, $this->cascade ? 'cascade' : 'no cascade'); + + if ($this->orphanRemoval) { + $name = "{$name} - (orphan removal)"; + } + + return $name; + } + + /** + * @param array{class: class-string, field: string} $source + */ + public static function fromArray(array $source, bool $cascade = false, bool $orphanRemoval = false): self + { + return new self(class: $source['class'], field: $source['field'], cascade: $cascade, orphanRemoval: $orphanRemoval); + } + + /** + * @param list $relationshipFields + * @return \Generator> + */ + public static function allCombinations(array $relationshipFields): iterable + { + // prevent too long test suite permutation when Dama is disabled + if (!\getenv('USE_DAMA_DOCTRINE_TEST_BUNDLE')) { + $metadata = self::fromArray($relationshipFields[0]); + + yield "{$metadata}\n" => [$metadata]; + + return; + } + + $total = 2 ** \count($relationshipFields); + + $hasOneToMany = false; + + for ($i = 0; $i < $total; ++$i) { + $temp = []; + + $permutationName = "\n"; + for ($j = 0; $j < \count($relationshipFields); ++$j) { + $metadata = self::fromArray($relationshipFields[$j], cascade: (bool) (($i >> $j) & 1)); + + $temp[] = $metadata; + $permutationName = "{$permutationName}$metadata\n"; + + if ($relationshipFields[$j]['isOneToMany']) { + $hasOneToMany = true; + } + } + + yield $permutationName => $temp; + } + + if (!$hasOneToMany) { + return; + } + + // if we have at least one OneToMany relationship, we need to test with orphan removal + // let's add only one permutation with orphan removal (and all cascade to true) + $temp = []; + $permutationName = "\n"; + foreach ($relationshipFields as $relationshipField) { + $metadata = self::fromArray($relationshipField, cascade: true, orphanRemoval: $relationshipField['isOneToMany']); + $temp[] = $metadata; + $permutationName = "{$permutationName}$metadata\n"; + } + yield $permutationName => $temp; + } +} diff --git a/tests/Fixture/DoctrineCascadeRelationship/PhpUnitTestExtension.php b/tests/Fixture/DoctrineCascadeRelationship/PhpUnitTestExtension.php new file mode 100644 index 000000000..787c407e6 --- /dev/null +++ b/tests/Fixture/DoctrineCascadeRelationship/PhpUnitTestExtension.php @@ -0,0 +1,49 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Tests\Fixture\DoctrineCascadeRelationship; + +use PHPUnit\Event; +use PHPUnit\Runner; +use PHPUnit\TextUI; + +/** + * @author Nicolas PHILIPPE + */ +final class PhpUnitTestExtension implements Runner\Extension\Extension, Event\Test\DataProviderMethodCalledSubscriber +{ + public function bootstrap( + TextUI\Configuration\Configuration $configuration, + Runner\Extension\Facade $facade, + Runner\Extension\ParameterCollection $parameters, + ): void { + $facade->registerSubscribers($this); + } + + public function notify(Event\Test\DataProviderMethodCalled $event): void + { + $testMethod = $event->testMethod(); + + $attributes = (new \ReflectionMethod($testMethod->className(), $testMethod->methodName()))->getAttributes(UsingRelationships::class); + + if (!$attributes) { + return; + } + + if (!\method_exists($testMethod->className(), 'setCurrentProvidedMethodName')) { + throw new \LogicException("Test \"{$testMethod->className()}::{$testMethod->methodName()}()\" should use trait ChangesEntityRelationshipCascadePersist."); + } + + $testMethod->className()::setCurrentProvidedMethodName($testMethod->methodName()); + } +} diff --git a/tests/Fixture/DoctrineCascadeRelationship/UsingRelationships.php b/tests/Fixture/DoctrineCascadeRelationship/UsingRelationships.php new file mode 100644 index 000000000..7041d271c --- /dev/null +++ b/tests/Fixture/DoctrineCascadeRelationship/UsingRelationships.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\Tests\Fixture\DoctrineCascadeRelationship; + +/** + * @author Nicolas PHILIPPE + */ +#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] +final class UsingRelationships +{ + public function __construct( + /** @var class-string */ + public readonly string $class, + public readonly array $relationShips, + ) { + } +} diff --git a/tests/Fixture/Document/DocumentWithReadonly.php b/tests/Fixture/Document/DocumentWithReadonly.php index 6393bdee4..8a775edf0 100644 --- a/tests/Fixture/Document/DocumentWithReadonly.php +++ b/tests/Fixture/Document/DocumentWithReadonly.php @@ -12,14 +12,12 @@ namespace Zenstruck\Foundry\Tests\Fixture\Document; use Doctrine\ODM\MongoDB\Mapping\Annotations as MongoDB; +use Zenstruck\Foundry\Tests\Fixture\Model\Base; use Zenstruck\Foundry\Tests\Fixture\Model\Embeddable; #[MongoDB\Document] -class DocumentWithReadonly +class DocumentWithReadonly extends Base { - #[MongoDB\Id(type: 'int', strategy: 'INCREMENT')] - public int $id; - public function __construct( #[MongoDB\Field()] public readonly int $prop, @@ -29,7 +27,6 @@ public function __construct( #[MongoDB\Field()] public readonly \DateTimeImmutable $date, - ) - { + ) { } } diff --git a/tests/Fixture/Document/DocumentWithUid.php b/tests/Fixture/Document/DocumentWithUid.php new file mode 100644 index 000000000..bd5830fc4 --- /dev/null +++ b/tests/Fixture/Document/DocumentWithUid.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Tests\Fixture\Document; + +use Doctrine\ODM\MongoDB\Mapping\Annotations as MongoDB; +use Symfony\Component\Uid\Uuid; + +/** + * @author Nicolas PHILIPPE + */ +#[MongoDB\Document] +class DocumentWithUid +{ + #[MongoDB\Id(type: 'bin_uuid', strategy: 'NONE')] + public string $id; + + public function __construct() + { + $this->id = Uuid::v7()->toBinary(); + } +} diff --git a/tests/Fixture/Entity/Address.php b/tests/Fixture/Entity/Address.php index 9cac62894..41627cd0d 100644 --- a/tests/Fixture/Entity/Address.php +++ b/tests/Fixture/Entity/Address.php @@ -17,9 +17,12 @@ /** * @author Kevin Bond */ -#[ORM\MappedSuperclass] -abstract class Address extends Base +#[ORM\Entity] +class Address extends Base { + #[ORM\OneToOne(targetEntity: Contact::class, mappedBy: 'address')] + protected ?Contact $contact = null; + #[ORM\Column(length: 255)] private string $city; @@ -28,6 +31,16 @@ public function __construct(string $city) $this->city = $city; } + public function getContact(): ?Contact + { + return $this->contact; + } + + public function setContact(?Contact $contact): void + { + $this->contact = $contact; + } + public function getCity(): string { return $this->city; diff --git a/tests/Fixture/Entity/Category.php b/tests/Fixture/Entity/Category.php index 0d2d6bb79..68003d289 100644 --- a/tests/Fixture/Entity/Category.php +++ b/tests/Fixture/Entity/Category.php @@ -19,13 +19,15 @@ /** * @author Kevin Bond */ -#[ORM\MappedSuperclass] -abstract class Category extends Base +#[ORM\Entity] +class Category extends Base { /** @var Collection */ + #[ORM\OneToMany(mappedBy: 'category', targetEntity: Contact::class)] protected Collection $contacts; /** @var Collection */ + #[ORM\OneToMany(mappedBy: 'secondaryCategory', targetEntity: Contact::class)] protected Collection $secondaryContacts; #[ORM\Column(length: 255)] diff --git a/tests/Fixture/Entity/Category/CascadeCategory.php b/tests/Fixture/Entity/Category/CascadeCategory.php deleted file mode 100644 index 66d7efa14..000000000 --- a/tests/Fixture/Entity/Category/CascadeCategory.php +++ /dev/null @@ -1,30 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Zenstruck\Foundry\Tests\Fixture\Entity\Category; - -use Doctrine\Common\Collections\Collection; -use Doctrine\ORM\Mapping as ORM; -use Zenstruck\Foundry\Tests\Fixture\Entity\Category; -use Zenstruck\Foundry\Tests\Fixture\Entity\Contact\CascadeContact; - -/** - * @author Kevin Bond - */ -#[ORM\Entity] -class CascadeCategory extends Category -{ - #[ORM\OneToMany(mappedBy: 'category', targetEntity: CascadeContact::class, cascade: ['persist', 'remove'])] - protected Collection $contacts; - - #[ORM\OneToMany(mappedBy: 'secondaryCategory', targetEntity: CascadeContact::class, cascade: ['persist', 'remove'])] - protected Collection $secondaryContacts; -} diff --git a/tests/Fixture/Entity/Category/StandardCategory.php b/tests/Fixture/Entity/Category/StandardCategory.php deleted file mode 100644 index d994dd02e..000000000 --- a/tests/Fixture/Entity/Category/StandardCategory.php +++ /dev/null @@ -1,30 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Zenstruck\Foundry\Tests\Fixture\Entity\Category; - -use Doctrine\Common\Collections\Collection; -use Doctrine\ORM\Mapping as ORM; -use Zenstruck\Foundry\Tests\Fixture\Entity\Category; -use Zenstruck\Foundry\Tests\Fixture\Entity\Contact\StandardContact; - -/** - * @author Kevin Bond - */ -#[ORM\Entity] -class StandardCategory extends Category -{ - #[ORM\OneToMany(mappedBy: 'category', targetEntity: StandardContact::class)] - protected Collection $contacts; - - #[ORM\OneToMany(mappedBy: 'secondaryCategory', targetEntity: StandardContact::class)] - protected Collection $secondaryContacts; -} diff --git a/tests/Fixture/Entity/Contact/ChildContact.php b/tests/Fixture/Entity/ChildContact.php similarity index 74% rename from tests/Fixture/Entity/Contact/ChildContact.php rename to tests/Fixture/Entity/ChildContact.php index 3305ed391..ef8916d5f 100644 --- a/tests/Fixture/Entity/Contact/ChildContact.php +++ b/tests/Fixture/Entity/ChildContact.php @@ -9,11 +9,11 @@ * file that was distributed with this source code. */ -namespace Zenstruck\Foundry\Tests\Fixture\Entity\Contact; +namespace Zenstruck\Foundry\Tests\Fixture\Entity; use Doctrine\ORM\Mapping as ORM; #[ORM\Entity] -class ChildContact extends StandardContact +class ChildContact extends Contact { } diff --git a/tests/Fixture/Entity/Contact.php b/tests/Fixture/Entity/Contact.php index fcafeb29b..1152ba79c 100644 --- a/tests/Fixture/Entity/Contact.php +++ b/tests/Fixture/Entity/Contact.php @@ -19,19 +19,30 @@ /** * @author Kevin Bond */ -#[ORM\MappedSuperclass] -abstract class Contact extends Base +#[ORM\Entity] +#[ORM\InheritanceType(value: 'SINGLE_TABLE')] +#[ORM\DiscriminatorColumn(name: 'type')] +#[ORM\DiscriminatorMap(['simple' => Contact::class, 'specific' => ChildContact::class])] +class Contact extends Base { + #[ORM\ManyToOne(targetEntity: Category::class, inversedBy: 'contacts')] + #[ORM\JoinColumn(nullable: true)] protected ?Category $category = null; + #[ORM\ManyToOne(targetEntity: Category::class, inversedBy: 'secondaryContacts')] protected ?Category $secondaryCategory = null; /** @var Collection */ + #[ORM\ManyToMany(targetEntity: Tag::class, inversedBy: 'contacts')] protected Collection $tags; /** @var Collection */ + #[ORM\ManyToMany(targetEntity: Tag::class, inversedBy: 'secondaryContacts')] + #[ORM\JoinTable(name: 'category_tag_standard_secondary')] protected Collection $secondaryTags; + #[ORM\OneToOne(targetEntity: Address::class, inversedBy: 'contact')] + #[ORM\JoinColumn(nullable: false)] protected Address $address; #[ORM\Column(length: 255)] diff --git a/tests/Fixture/Entity/Contact/CascadeContact.php b/tests/Fixture/Entity/Contact/CascadeContact.php deleted file mode 100644 index f0f4f1357..000000000 --- a/tests/Fixture/Entity/Contact/CascadeContact.php +++ /dev/null @@ -1,46 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Zenstruck\Foundry\Tests\Fixture\Entity\Contact; - -use Doctrine\Common\Collections\Collection; -use Doctrine\ORM\Mapping as ORM; -use Zenstruck\Foundry\Tests\Fixture\Entity\Address; -use Zenstruck\Foundry\Tests\Fixture\Entity\Address\CascadeAddress; -use Zenstruck\Foundry\Tests\Fixture\Entity\Category; -use Zenstruck\Foundry\Tests\Fixture\Entity\Category\CascadeCategory; -use Zenstruck\Foundry\Tests\Fixture\Entity\Contact; -use Zenstruck\Foundry\Tests\Fixture\Entity\Tag\CascadeTag; - -/** - * @author Kevin Bond - */ -#[ORM\Entity] -class CascadeContact extends Contact -{ - #[ORM\ManyToOne(targetEntity: CascadeCategory::class, cascade: ['persist', 'remove'], inversedBy: 'contacts')] - #[ORM\JoinColumn(nullable: true)] - protected ?Category $category = null; - - #[ORM\ManyToOne(targetEntity: CascadeCategory::class, cascade: ['persist', 'remove'], inversedBy: 'secondaryContacts')] - protected ?Category $secondaryCategory = null; - - #[ORM\ManyToMany(targetEntity: CascadeTag::class, inversedBy: 'contacts', cascade: ['persist', 'remove'])] - protected Collection $tags; - - #[ORM\ManyToMany(targetEntity: CascadeTag::class, inversedBy: 'secondaryContacts', cascade: ['persist', 'remove'])] - #[ORM\JoinTable(name: 'category_tag_cascade_secondary')] - protected Collection $secondaryTags; - - #[ORM\OneToOne(targetEntity: CascadeAddress::class, cascade: ['persist', 'remove'])] - #[ORM\JoinColumn(nullable: false)] - protected Address $address; -} diff --git a/tests/Fixture/Entity/Contact/StandardContact.php b/tests/Fixture/Entity/Contact/StandardContact.php deleted file mode 100644 index cd3f613a5..000000000 --- a/tests/Fixture/Entity/Contact/StandardContact.php +++ /dev/null @@ -1,50 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Zenstruck\Foundry\Tests\Fixture\Entity\Contact; - -use Doctrine\Common\Collections\Collection; -use Doctrine\ORM\Mapping as ORM; -use Zenstruck\Foundry\Tests\Fixture\Entity\Address; -use Zenstruck\Foundry\Tests\Fixture\Entity\Address\StandardAddress; -use Zenstruck\Foundry\Tests\Fixture\Entity\Category; -use Zenstruck\Foundry\Tests\Fixture\Entity\Category\StandardCategory; -use Zenstruck\Foundry\Tests\Fixture\Entity\Contact; -use Zenstruck\Foundry\Tests\Fixture\Entity\Tag\StandardTag; - -/** - * @author Kevin Bond - */ -#[ORM\Entity] -#[ORM\Table(name: 'posts')] -#[ORM\InheritanceType(value: 'SINGLE_TABLE')] -#[ORM\DiscriminatorColumn(name: 'type')] -#[ORM\DiscriminatorMap(['simple' => StandardContact::class, 'specific' => ChildContact::class])] -class StandardContact extends Contact -{ - #[ORM\ManyToOne(targetEntity: StandardCategory::class, inversedBy: 'contacts')] - #[ORM\JoinColumn(nullable: true)] - protected ?Category $category = null; - - #[ORM\ManyToOne(targetEntity: StandardCategory::class, inversedBy: 'secondaryContacts')] - protected ?Category $secondaryCategory = null; - - #[ORM\ManyToMany(targetEntity: StandardTag::class, inversedBy: 'contacts')] - protected Collection $tags; - - #[ORM\ManyToMany(targetEntity: StandardTag::class, inversedBy: 'secondaryContacts')] - #[ORM\JoinTable(name: 'category_tag_standard_secondary')] - protected Collection $secondaryTags; - - #[ORM\OneToOne(targetEntity: StandardAddress::class)] - #[ORM\JoinColumn(nullable: false)] - protected Address $address; -} diff --git a/tests/Fixture/Entity/EdgeCases/EntityWithReadonly/EntityWithReadonly.php b/tests/Fixture/Entity/EdgeCases/EntityWithReadonly/EntityWithReadonly.php index 2d9881812..52cce4dde 100644 --- a/tests/Fixture/Entity/EdgeCases/EntityWithReadonly/EntityWithReadonly.php +++ b/tests/Fixture/Entity/EdgeCases/EntityWithReadonly/EntityWithReadonly.php @@ -32,7 +32,6 @@ public function __construct( #[ORM\Column()] public readonly \DateTimeImmutable $date, - ) - { + ) { } } diff --git a/tests/Fixture/Entity/EdgeCases/IndexedOneToMany/Child.php b/tests/Fixture/Entity/EdgeCases/IndexedOneToMany/Child.php new file mode 100644 index 000000000..0bcf49c12 --- /dev/null +++ b/tests/Fixture/Entity/EdgeCases/IndexedOneToMany/Child.php @@ -0,0 +1,35 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Tests\Fixture\Entity\EdgeCases\IndexedOneToMany; + +use Doctrine\ORM\Mapping as ORM; +use Zenstruck\Foundry\Tests\Fixture\Model\Base; + +/** + * @author Nicolas PHILIPPE + */ +#[ORM\Entity] +#[ORM\Table('index_by_one_to_many_level_1')] +class Child extends Base +{ + public function __construct( + #[ORM\ManyToOne(inversedBy: 'items')] + #[ORM\JoinColumn(nullable: false)] + public ParentEntity $parent, + + #[ORM\Column()] + public string $language, + ) { + } +} diff --git a/tests/Fixture/Entity/EdgeCases/IndexedOneToMany/ParentEntity.php b/tests/Fixture/Entity/EdgeCases/IndexedOneToMany/ParentEntity.php new file mode 100644 index 000000000..4ddc7b58d --- /dev/null +++ b/tests/Fixture/Entity/EdgeCases/IndexedOneToMany/ParentEntity.php @@ -0,0 +1,58 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Tests\Fixture\Entity\EdgeCases\IndexedOneToMany; + +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; +use Doctrine\ORM\Mapping as ORM; +use Zenstruck\Foundry\Tests\Fixture\Model\Base; + +/** + * @author Nicolas PHILIPPE + */ +#[ORM\Entity] +#[ORM\Table('index_by_one_to_many_parent')] +class ParentEntity extends Base +{ + /** @var Collection */ + #[ORM\OneToMany(targetEntity: Child::class, mappedBy: 'parent', indexBy: 'language')] + private Collection $items; + + public function __construct() + { + $this->items = new ArrayCollection(); + } + + /** + * @return Collection + */ + public function getItems(): Collection + { + return $this->items; + } + + public function addItem(Child $item): void + { + if (!$this->items->contains($item)) { + $this->items->add($item); + } + } + + public function removeItem(Child $item): void + { + if ($this->items->contains($item)) { + $this->items->removeElement($item); + } + } +} diff --git a/tests/Fixture/Entity/EdgeCases/InversedOneToOneWithNonNullableOwning/InverseSide.php b/tests/Fixture/Entity/EdgeCases/InversedOneToOneWithNonNullableOwning/InverseSide.php new file mode 100644 index 000000000..f93863fba --- /dev/null +++ b/tests/Fixture/Entity/EdgeCases/InversedOneToOneWithNonNullableOwning/InverseSide.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\Fixture\Entity\EdgeCases\InversedOneToOneWithNonNullableOwning; + +use Doctrine\ORM\Mapping as ORM; +use Zenstruck\Foundry\Tests\Fixture\Model\Base; + +/** + * @author Nicolas PHILIPPE + */ +#[ORM\Entity] +#[ORM\Table('inversed_one_to_one_non_nullable_owning_inverse_side')] +class InverseSide extends Base +{ + public function __construct( + #[ORM\OneToOne(mappedBy: 'inverseSide')] // @phpstan-ignore doctrine.associationType + public OwningSide $owningSide, + ) { + } +} diff --git a/tests/Fixture/Entity/EdgeCases/RelationshipWithGlobalEntity/CascadeRelationshipWithGlobalEntity.php b/tests/Fixture/Entity/EdgeCases/InversedOneToOneWithNonNullableOwning/OwningSide.php similarity index 51% rename from tests/Fixture/Entity/EdgeCases/RelationshipWithGlobalEntity/CascadeRelationshipWithGlobalEntity.php rename to tests/Fixture/Entity/EdgeCases/InversedOneToOneWithNonNullableOwning/OwningSide.php index 233333f6c..a70330983 100644 --- a/tests/Fixture/Entity/EdgeCases/RelationshipWithGlobalEntity/CascadeRelationshipWithGlobalEntity.php +++ b/tests/Fixture/Entity/EdgeCases/InversedOneToOneWithNonNullableOwning/OwningSide.php @@ -11,17 +11,18 @@ * file that was distributed with this source code. */ -namespace Zenstruck\Foundry\Tests\Fixture\Entity\EdgeCases\RelationshipWithGlobalEntity; +namespace Zenstruck\Foundry\Tests\Fixture\Entity\EdgeCases\InversedOneToOneWithNonNullableOwning; use Doctrine\ORM\Mapping as ORM; -use Zenstruck\Foundry\Tests\Fixture\Entity\GlobalEntity; +use Zenstruck\Foundry\Tests\Fixture\Model\Base; /** * @author Nicolas PHILIPPE */ #[ORM\Entity] -class CascadeRelationshipWithGlobalEntity extends RelationshipWithGlobalEntity +#[ORM\Table('inversed_one_to_one_with_non_nullable_owning_owning_side')] +class OwningSide extends Base { - #[ORM\ManyToOne(targetEntity: GlobalEntity::class, cascade: ['persist'])] - protected ?GlobalEntity $globalEntity = null; + #[ORM\OneToOne(inversedBy: 'owningSide')] + public ?InverseSide $inverseSide = null; } diff --git a/tests/Fixture/Entity/EdgeCases/InversedOneToOneWithOneToMany/InverseSide.php b/tests/Fixture/Entity/EdgeCases/InversedOneToOneWithOneToMany/InverseSide.php new file mode 100644 index 000000000..edec900ba --- /dev/null +++ b/tests/Fixture/Entity/EdgeCases/InversedOneToOneWithOneToMany/InverseSide.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\Tests\Fixture\Entity\EdgeCases\InversedOneToOneWithOneToMany; + +use Doctrine\ORM\Mapping as ORM; +use Zenstruck\Foundry\Tests\Fixture\Model\Base; + +/** + * @author Nicolas PHILIPPE + */ +#[ORM\Entity] +#[ORM\Table('inversed_one_to_one_with_one_to_many_inverse_side')] +class InverseSide extends Base +{ + #[ORM\OneToOne(mappedBy: 'inverseSide')] + private ?OwningSide $owningSide = null; + + public function getOwningSide(): ?OwningSide + { + return $this->owningSide; + } + + public function setOwningSide(OwningSide $owningSide): void + { + $this->owningSide = $owningSide; + $owningSide->inverseSide = $this; + } +} diff --git a/tests/Fixture/Entity/EdgeCases/RelationshipWithGlobalEntity/StandardRelationshipWithGlobalEntity.php b/tests/Fixture/Entity/EdgeCases/InversedOneToOneWithOneToMany/Item.php similarity index 53% rename from tests/Fixture/Entity/EdgeCases/RelationshipWithGlobalEntity/StandardRelationshipWithGlobalEntity.php rename to tests/Fixture/Entity/EdgeCases/InversedOneToOneWithOneToMany/Item.php index 180a4b19f..dfdf2aca6 100644 --- a/tests/Fixture/Entity/EdgeCases/RelationshipWithGlobalEntity/StandardRelationshipWithGlobalEntity.php +++ b/tests/Fixture/Entity/EdgeCases/InversedOneToOneWithOneToMany/Item.php @@ -11,17 +11,18 @@ * file that was distributed with this source code. */ -namespace Zenstruck\Foundry\Tests\Fixture\Entity\EdgeCases\RelationshipWithGlobalEntity; +namespace Zenstruck\Foundry\Tests\Fixture\Entity\EdgeCases\InversedOneToOneWithOneToMany; use Doctrine\ORM\Mapping as ORM; -use Zenstruck\Foundry\Tests\Fixture\Entity\GlobalEntity; +use Zenstruck\Foundry\Tests\Fixture\Model\Base; /** * @author Nicolas PHILIPPE */ #[ORM\Entity] -class StandardRelationshipWithGlobalEntity extends RelationshipWithGlobalEntity +#[ORM\Table('inversed_one_to_one_with_one_to_many_item_if_collection')] +class Item extends Base { - #[ORM\ManyToOne(targetEntity: GlobalEntity::class)] - protected ?GlobalEntity $globalEntity = null; + #[ORM\ManyToOne(inversedBy: 'items')] + public ?OwningSide $owningSide = null; } diff --git a/tests/Fixture/Entity/EdgeCases/InversedOneToOneWithOneToMany/OwningSide.php b/tests/Fixture/Entity/EdgeCases/InversedOneToOneWithOneToMany/OwningSide.php new file mode 100644 index 000000000..6506f8159 --- /dev/null +++ b/tests/Fixture/Entity/EdgeCases/InversedOneToOneWithOneToMany/OwningSide.php @@ -0,0 +1,63 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Tests\Fixture\Entity\EdgeCases\InversedOneToOneWithOneToMany; + +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; +use Doctrine\ORM\Mapping as ORM; +use Zenstruck\Foundry\Tests\Fixture\Model\Base; + +/** + * @author Nicolas PHILIPPE + */ +#[ORM\Entity] +#[ORM\Table('inversed_one_to_one_with_one_to_many_owning_side')] +class OwningSide extends Base +{ + #[ORM\OneToOne(inversedBy: 'owningSide')] + public ?InverseSide $inverseSide = null; + + /** @var Collection */ + #[ORM\OneToMany(targetEntity: Item::class, mappedBy: 'owningSide')] + private Collection $items; + + public function __construct() + { + $this->items = new ArrayCollection(); + } + + /** + * @return Collection + */ + public function getItems(): Collection + { + return $this->items; + } + + public function addItem(Item $item): void + { + if (!$this->items->contains($item)) { + $this->items->add($item); + $item->owningSide = $this; + } + } + + public function removeItem(Item $item): void + { + if ($this->items->contains($item)) { + $this->items->removeElement($item); + $item->owningSide = null; + } + } +} diff --git a/tests/Fixture/Entity/EdgeCases/InversedOneToOneWithSetter/InverseSide.php b/tests/Fixture/Entity/EdgeCases/InversedOneToOneWithSetter/InverseSide.php new file mode 100644 index 000000000..bc3b91134 --- /dev/null +++ b/tests/Fixture/Entity/EdgeCases/InversedOneToOneWithSetter/InverseSide.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\Tests\Fixture\Entity\EdgeCases\InversedOneToOneWithSetter; + +use Doctrine\ORM\Mapping as ORM; +use Zenstruck\Foundry\Tests\Fixture\Model\Base; + +/** + * @author Nicolas PHILIPPE + */ +#[ORM\Entity] +#[ORM\Table('inversed_one_to_one_with_setter_inverse_side')] +class InverseSide extends Base +{ + #[ORM\OneToOne(mappedBy: 'inverseSide')] + private ?OwningSide $owningSide = null; + + public function getOwningSide(): ?OwningSide + { + return $this->owningSide; + } + + public function setOwningSide(OwningSide $owningSide): void + { + $this->owningSide = $owningSide; + $owningSide->inverseSide = $this; + } +} diff --git a/tests/Fixture/Entity/EdgeCases/InversedOneToOneWithSetter/OwningSide.php b/tests/Fixture/Entity/EdgeCases/InversedOneToOneWithSetter/OwningSide.php new file mode 100644 index 000000000..1c8460f78 --- /dev/null +++ b/tests/Fixture/Entity/EdgeCases/InversedOneToOneWithSetter/OwningSide.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\Tests\Fixture\Entity\EdgeCases\InversedOneToOneWithSetter; + +use Doctrine\ORM\Mapping as ORM; +use Zenstruck\Foundry\Tests\Fixture\Model\Base; + +/** + * @author Nicolas PHILIPPE + */ +#[ORM\Entity] +#[ORM\Table('inversed_one_to_one_with_setter_owning_side')] +class OwningSide extends Base +{ + #[ORM\OneToOne(inversedBy: 'owningSide')] + public ?InverseSide $inverseSide = null; +} diff --git a/tests/Fixture/Entity/EdgeCases/ManyToOneToSelfReferencing/OwningSide.php b/tests/Fixture/Entity/EdgeCases/ManyToOneToSelfReferencing/OwningSide.php new file mode 100644 index 000000000..4cc857183 --- /dev/null +++ b/tests/Fixture/Entity/EdgeCases/ManyToOneToSelfReferencing/OwningSide.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\Tests\Fixture\Entity\EdgeCases\ManyToOneToSelfReferencing; + +use Doctrine\ORM\Mapping as ORM; + +/** + * @author Nicolas PHILIPPE + */ +#[ORM\Entity] +#[ORM\Table('many_to_one_to_self_referencing_owning_side')] +class OwningSide +{ + #[ORM\Id] + #[ORM\Column] + #[ORM\GeneratedValue(strategy: 'AUTO')] + public ?int $id = null; + + #[ORM\ManyToOne(inversedBy: 'owningSides')] + public ?SelfReferencingInverseSide $inverseSide = null; +} diff --git a/tests/Fixture/Entity/EdgeCases/ManyToOneToSelfReferencing/SelfReferencingInverseSide.php b/tests/Fixture/Entity/EdgeCases/ManyToOneToSelfReferencing/SelfReferencingInverseSide.php new file mode 100644 index 000000000..3c1dc248c --- /dev/null +++ b/tests/Fixture/Entity/EdgeCases/ManyToOneToSelfReferencing/SelfReferencingInverseSide.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\Tests\Fixture\Entity\EdgeCases\ManyToOneToSelfReferencing; + +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; +use Doctrine\ORM\Mapping as ORM; +use Zenstruck\Foundry\Tests\Fixture\Model\Base; + +/** + * @author Nicolas PHILIPPE + */ +#[ORM\Entity] +#[ORM\Table('many_to_one_to_self_referencing_inverse_side')] +class SelfReferencingInverseSide extends Base +{ + #[ORM\ManyToOne()] + public ?SelfReferencingInverseSide $inverseSide = null; + + /** @var Collection */ + #[ORM\OneToMany(targetEntity: OwningSide::class, mappedBy: 'inverseSide')] + public Collection $owningSides; + + public function __construct() + { + $this->owningSides = new ArrayCollection(); + } +} diff --git a/tests/Fixture/Entity/EdgeCases/MultipleMandatoryRelationshipToSameEntity/OwningSideEntity.php b/tests/Fixture/Entity/EdgeCases/MultipleMandatoryRelationshipToSameEntity/OwningSideEntity.php index f2f1c93d3..44607a149 100644 --- a/tests/Fixture/Entity/EdgeCases/MultipleMandatoryRelationshipToSameEntity/OwningSideEntity.php +++ b/tests/Fixture/Entity/EdgeCases/MultipleMandatoryRelationshipToSameEntity/OwningSideEntity.php @@ -21,8 +21,10 @@ class OwningSideEntity extends Base { public function __construct( #[ORM\ManyToOne(targetEntity: InversedSideEntity::class, cascade: ['persist', 'remove'], inversedBy: 'mainRelations')] + #[ORM\JoinColumn(nullable: false)] private InversedSideEntity $main, #[ORM\ManyToOne(targetEntity: InversedSideEntity::class, cascade: ['persist', 'remove'], inversedBy: 'secondaryRelations')] + #[ORM\JoinColumn(nullable: false)] private InversedSideEntity $secondary, ) { } diff --git a/tests/Fixture/Entity/EdgeCases/RelationshipWithGlobalEntity/RelationshipWithGlobalEntity.php b/tests/Fixture/Entity/EdgeCases/RelationshipWithGlobalEntity/RelationshipWithGlobalEntity.php index 677a678b1..83be62ae2 100644 --- a/tests/Fixture/Entity/EdgeCases/RelationshipWithGlobalEntity/RelationshipWithGlobalEntity.php +++ b/tests/Fixture/Entity/EdgeCases/RelationshipWithGlobalEntity/RelationshipWithGlobalEntity.php @@ -15,18 +15,15 @@ use Doctrine\ORM\Mapping as ORM; use Zenstruck\Foundry\Tests\Fixture\Entity\GlobalEntity; +use Zenstruck\Foundry\Tests\Fixture\Model\Base; /** * @author Nicolas PHILIPPE */ -#[ORM\MappedSuperclass] -abstract class RelationshipWithGlobalEntity +#[ORM\Entity] +class RelationshipWithGlobalEntity extends Base { - #[ORM\Id] - #[ORM\Column] - #[ORM\GeneratedValue(strategy: 'AUTO')] - public ?int $id = null; - + #[ORM\ManyToOne(targetEntity: GlobalEntity::class)] protected ?GlobalEntity $globalEntity = null; public function setGlobalEntity(?GlobalEntity $globalEntity): void diff --git a/tests/Fixture/Entity/EdgeCases/RichDomainMandatoryRelationship/CascadeInversedSideEntity.php b/tests/Fixture/Entity/EdgeCases/RichDomainMandatoryRelationship/CascadeInversedSideEntity.php deleted file mode 100644 index aa417ad59..000000000 --- a/tests/Fixture/Entity/EdgeCases/RichDomainMandatoryRelationship/CascadeInversedSideEntity.php +++ /dev/null @@ -1,24 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Zenstruck\Foundry\Tests\Fixture\Entity\EdgeCases\RichDomainMandatoryRelationship; - -use Doctrine\Common\Collections\Collection; -use Doctrine\ORM\Mapping as ORM; - -#[ORM\Entity()] -class CascadeInversedSideEntity extends InversedSideEntity -{ - #[ORM\OneToMany(targetEntity: CascadeOwningSideEntity::class, mappedBy: 'main', cascade: ['persist'])] - protected Collection $relations; -} diff --git a/tests/Fixture/Entity/EdgeCases/RichDomainMandatoryRelationship/CascadeOwningSideEntity.php b/tests/Fixture/Entity/EdgeCases/RichDomainMandatoryRelationship/CascadeOwningSideEntity.php deleted file mode 100644 index ccf81e3b9..000000000 --- a/tests/Fixture/Entity/EdgeCases/RichDomainMandatoryRelationship/CascadeOwningSideEntity.php +++ /dev/null @@ -1,29 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Zenstruck\Foundry\Tests\Fixture\Entity\EdgeCases\RichDomainMandatoryRelationship; - -use Doctrine\ORM\Mapping as ORM; - -#[ORM\Entity] -class CascadeOwningSideEntity extends OwningSideEntity -{ - #[ORM\ManyToOne(targetEntity: CascadeInversedSideEntity::class, cascade: ['persist'], inversedBy: 'relations')] - protected InversedSideEntity $main; - - public function __construct( - InversedSideEntity $main, - ) { - parent::__construct($main); - } -} diff --git a/tests/Fixture/Entity/EdgeCases/RichDomainMandatoryRelationship/InversedSideEntity.php b/tests/Fixture/Entity/EdgeCases/RichDomainMandatoryRelationship/InversedSide.php similarity index 77% rename from tests/Fixture/Entity/EdgeCases/RichDomainMandatoryRelationship/InversedSideEntity.php rename to tests/Fixture/Entity/EdgeCases/RichDomainMandatoryRelationship/InversedSide.php index 614619b9a..598a198f7 100644 --- a/tests/Fixture/Entity/EdgeCases/RichDomainMandatoryRelationship/InversedSideEntity.php +++ b/tests/Fixture/Entity/EdgeCases/RichDomainMandatoryRelationship/InversedSide.php @@ -18,11 +18,12 @@ use Doctrine\ORM\Mapping as ORM; use Zenstruck\Foundry\Tests\Fixture\Model\Base; -#[ORM\MappedSuperclass] -#[ORM\Table(name: 'rich_domain_mandatory_relationship_inversed_side_entity')] -abstract class InversedSideEntity extends Base +#[ORM\Entity] +#[ORM\Table(name: 'rich_domain_mandatory_relationship_inversed_side')] +class InversedSide extends Base { - /** @var Collection */ + /** @var Collection */ + #[ORM\OneToMany(targetEntity: OwningSide::class, mappedBy: 'main')] protected Collection $relations; public function __construct() @@ -31,14 +32,14 @@ public function __construct() } /** - * @return Collection + * @return Collection */ public function getRelations(): Collection { return $this->relations; } - public function addRelation(OwningSideEntity $relation): static + public function addRelation(OwningSide $relation): static { if (!$this->relations->contains($relation)) { $this->relations->add($relation); @@ -47,7 +48,7 @@ public function addRelation(OwningSideEntity $relation): static return $this; } - public function removeRelation(OwningSideEntity $relation): static + public function removeRelation(OwningSide $relation): static { if ($this->relations->contains($relation)) { $this->relations->removeElement($relation); diff --git a/tests/Fixture/Entity/EdgeCases/RichDomainMandatoryRelationship/OwningSideEntity.php b/tests/Fixture/Entity/EdgeCases/RichDomainMandatoryRelationship/OwningSide.php similarity index 71% rename from tests/Fixture/Entity/EdgeCases/RichDomainMandatoryRelationship/OwningSideEntity.php rename to tests/Fixture/Entity/EdgeCases/RichDomainMandatoryRelationship/OwningSide.php index e5d8a314c..a73fbbd39 100644 --- a/tests/Fixture/Entity/EdgeCases/RichDomainMandatoryRelationship/OwningSideEntity.php +++ b/tests/Fixture/Entity/EdgeCases/RichDomainMandatoryRelationship/OwningSide.php @@ -16,17 +16,19 @@ use Doctrine\ORM\Mapping as ORM; use Zenstruck\Foundry\Tests\Fixture\Model\Base; -#[ORM\MappedSuperclass] -#[ORM\Table(name: 'rich_domain_mandatory_relationship_owning_side_entity')] -abstract class OwningSideEntity extends Base +#[ORM\Entity] +#[ORM\Table(name: 'rich_domain_mandatory_relationship_owning_side')] +class OwningSide extends Base { public function __construct( - protected InversedSideEntity $main, + #[ORM\ManyToOne(targetEntity: InversedSide::class, inversedBy: 'relations')] + #[ORM\JoinColumn(nullable: false)] + private InversedSide $main, ) { $main->addRelation($this); } - public function getMain(): InversedSideEntity + public function getMain(): InversedSide { return $this->main; } diff --git a/tests/Fixture/Entity/EdgeCases/RichDomainMandatoryRelationship/StandardInversedSideEntity.php b/tests/Fixture/Entity/EdgeCases/RichDomainMandatoryRelationship/StandardInversedSideEntity.php deleted file mode 100644 index fb83d6880..000000000 --- a/tests/Fixture/Entity/EdgeCases/RichDomainMandatoryRelationship/StandardInversedSideEntity.php +++ /dev/null @@ -1,24 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Zenstruck\Foundry\Tests\Fixture\Entity\EdgeCases\RichDomainMandatoryRelationship; - -use Doctrine\Common\Collections\Collection; -use Doctrine\ORM\Mapping as ORM; - -#[ORM\Entity()] -class StandardInversedSideEntity extends InversedSideEntity -{ - #[ORM\OneToMany(targetEntity: StandardOwningSideEntity::class, mappedBy: 'main')] - protected Collection $relations; -} diff --git a/tests/Fixture/Entity/EdgeCases/RichDomainMandatoryRelationship/StandardOwningSideEntity.php b/tests/Fixture/Entity/EdgeCases/RichDomainMandatoryRelationship/StandardOwningSideEntity.php deleted file mode 100644 index e48d05922..000000000 --- a/tests/Fixture/Entity/EdgeCases/RichDomainMandatoryRelationship/StandardOwningSideEntity.php +++ /dev/null @@ -1,29 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Zenstruck\Foundry\Tests\Fixture\Entity\EdgeCases\RichDomainMandatoryRelationship; - -use Doctrine\ORM\Mapping as ORM; - -#[ORM\Entity] -class StandardOwningSideEntity extends OwningSideEntity -{ - #[ORM\ManyToOne(targetEntity: StandardInversedSideEntity::class, inversedBy: 'relations')] - protected InversedSideEntity $main; - - public function __construct( - InversedSideEntity $main, - ) { - parent::__construct($main); - } -} diff --git a/tests/Fixture/Entity/Address/CascadeAddress.php b/tests/Fixture/Entity/EntityForEventListeners.php similarity index 52% rename from tests/Fixture/Entity/Address/CascadeAddress.php rename to tests/Fixture/Entity/EntityForEventListeners.php index 4eb75e627..4d36182b5 100644 --- a/tests/Fixture/Entity/Address/CascadeAddress.php +++ b/tests/Fixture/Entity/EntityForEventListeners.php @@ -1,5 +1,7 @@ - */ #[ORM\Entity] -class CascadeAddress extends Address +class EntityForEventListeners extends Base { + public function __construct( + #[ORM\Column()] + public string $name, + ) { + } } diff --git a/tests/Fixture/Entity/EntityWithUid.php b/tests/Fixture/Entity/EntityWithUid.php new file mode 100644 index 000000000..3a695b5b5 --- /dev/null +++ b/tests/Fixture/Entity/EntityWithUid.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Tests\Fixture\Entity; + +use Doctrine\ORM\Mapping as ORM; +use Symfony\Component\Uid\Uuid; + +/** + * @author Nicolas PHILIPPE + */ +#[ORM\Entity] +class EntityWithUid +{ + #[ORM\Id] + #[ORM\Column(type: 'uuid')] + public Uuid $id; + + public function __construct() + { + $this->id = Uuid::v7(); + } +} diff --git a/tests/Fixture/Entity/Tag.php b/tests/Fixture/Entity/Tag.php index 0cd7b861b..38e523512 100644 --- a/tests/Fixture/Entity/Tag.php +++ b/tests/Fixture/Entity/Tag.php @@ -19,13 +19,15 @@ /** * @author Kevin Bond */ -#[ORM\MappedSuperclass] -abstract class Tag extends Base +#[ORM\Entity] +class Tag extends Base { /** @var Collection */ + #[ORM\ManyToMany(targetEntity: Contact::class, mappedBy: 'tags', fetch: 'EAGER')] protected Collection $contacts; /** @var Collection */ + #[ORM\ManyToMany(targetEntity: Contact::class, mappedBy: 'secondaryTags')] protected Collection $secondaryContacts; #[ORM\Column(length: 255)] diff --git a/tests/Fixture/Entity/Tag/CascadeTag.php b/tests/Fixture/Entity/Tag/CascadeTag.php deleted file mode 100644 index 08afb283e..000000000 --- a/tests/Fixture/Entity/Tag/CascadeTag.php +++ /dev/null @@ -1,30 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Zenstruck\Foundry\Tests\Fixture\Entity\Tag; - -use Doctrine\Common\Collections\Collection; -use Doctrine\ORM\Mapping as ORM; -use Zenstruck\Foundry\Tests\Fixture\Entity\Contact\CascadeContact; -use Zenstruck\Foundry\Tests\Fixture\Entity\Tag; - -/** - * @author Kevin Bond - */ -#[ORM\Entity] -class CascadeTag extends Tag -{ - #[ORM\ManyToMany(targetEntity: CascadeContact::class, mappedBy: 'tags', cascade: ['persist', 'remove'], fetch: 'EAGER')] - protected Collection $contacts; - - #[ORM\ManyToMany(targetEntity: CascadeContact::class, mappedBy: 'secondaryTags', cascade: ['persist', 'remove'])] - protected Collection $secondaryContacts; -} diff --git a/tests/Fixture/Entity/Tag/StandardTag.php b/tests/Fixture/Entity/Tag/StandardTag.php deleted file mode 100644 index 7433aa3a0..000000000 --- a/tests/Fixture/Entity/Tag/StandardTag.php +++ /dev/null @@ -1,30 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Zenstruck\Foundry\Tests\Fixture\Entity\Tag; - -use Doctrine\Common\Collections\Collection; -use Doctrine\ORM\Mapping as ORM; -use Zenstruck\Foundry\Tests\Fixture\Entity\Contact\StandardContact; -use Zenstruck\Foundry\Tests\Fixture\Entity\Tag; - -/** - * @author Kevin Bond - */ -#[ORM\Entity] -class StandardTag extends Tag -{ - #[ORM\ManyToMany(targetEntity: StandardContact::class, mappedBy: 'tags', fetch: 'EAGER')] - protected Collection $contacts; - - #[ORM\ManyToMany(targetEntity: StandardContact::class, mappedBy: 'secondaryTags')] - protected Collection $secondaryContacts; -} diff --git a/tests/Fixture/EdgeCases/Migrate/ORM/EntityInAnotherSchema/Article.php b/tests/Fixture/EntityInAnotherSchema/Article.php similarity index 68% rename from tests/Fixture/EdgeCases/Migrate/ORM/EntityInAnotherSchema/Article.php rename to tests/Fixture/EntityInAnotherSchema/Article.php index fba361efc..62fcf3c3c 100644 --- a/tests/Fixture/EdgeCases/Migrate/ORM/EntityInAnotherSchema/Article.php +++ b/tests/Fixture/EntityInAnotherSchema/Article.php @@ -11,11 +11,17 @@ * file that was distributed with this source code. */ -namespace Zenstruck\Foundry\Tests\Fixture\EdgeCases\Migrate\ORM\EntityInAnotherSchema; +namespace Zenstruck\Foundry\Tests\Fixture\EntityInAnotherSchema; use Doctrine\ORM\Mapping as ORM; use Zenstruck\Foundry\Tests\Fixture\Model\Base; +/** + * Create custom "cms" schema ({@see Article}) to ensure "migrate" mode is still working with multiple schemas. + * Note: this entity is added to mapping only for PostgreSQL, as it is the only supported DBMS which handles multiple schemas. + * + * @see https://github.com/zenstruck/foundry/issues/618 + */ #[ORM\Entity] #[ORM\Table(name: 'article', schema: 'cms')] class Article extends Base diff --git a/tests/Fixture/Events/FactoryWithEventListeners.php b/tests/Fixture/Events/FactoryWithEventListeners.php new file mode 100644 index 000000000..dcda63c7b --- /dev/null +++ b/tests/Fixture/Events/FactoryWithEventListeners.php @@ -0,0 +1,38 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Tests\Fixture\Events; + +use Zenstruck\Foundry\Persistence\PersistentObjectFactory; +use Zenstruck\Foundry\Tests\Fixture\Entity\EntityForEventListeners; + +/** + * @extends PersistentObjectFactory + */ +final class FactoryWithEventListeners extends PersistentObjectFactory +{ + public static function class(): string + { + return EntityForEventListeners::class; + } + + /** + * @return array + */ + protected function defaults(): array + { + return [ + 'name' => self::faker()->sentence(), + ]; + } +} diff --git a/tests/Fixture/Events/FoundryEventListener.php b/tests/Fixture/Events/FoundryEventListener.php new file mode 100644 index 000000000..c9df24006 --- /dev/null +++ b/tests/Fixture/Events/FoundryEventListener.php @@ -0,0 +1,119 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Tests\Fixture\Events; + +use Symfony\Component\EventDispatcher\Attribute\AsEventListener; +use Zenstruck\Foundry\Attribute\AsFoundryHook; +use Zenstruck\Foundry\Object\Event\AfterInstantiate; +use Zenstruck\Foundry\Object\Event\BeforeInstantiate; +use Zenstruck\Foundry\Object\Event\Event; +use Zenstruck\Foundry\Persistence\Event\AfterPersist; +use Zenstruck\Foundry\Tests\Fixture\Entity\EntityForEventListeners; + +final class FoundryEventListener +{ + /** @param BeforeInstantiate $event */ + #[AsEventListener] + public function beforeInstantiate(BeforeInstantiate $event): void + { + if (EntityForEventListeners::class !== $event->objectClass) { + return; + } + + $event->parameters['name'] = $this->name($event->parameters['name'], $event); + } + + /** @param AfterInstantiate $event */ + #[AsEventListener] + public function afterInstantiate(AfterInstantiate $event): void + { + if (!$event->object instanceof EntityForEventListeners) { + return; + } + + $event->object->name = $this->name($event->object->name, $event); + } + + /** @param AfterPersist $event */ + #[AsEventListener] + public function afterPersist(AfterPersist $event): void + { + if (!$event->object instanceof EntityForEventListeners) { + return; + } + + $event->object->name = $this->name($event->object->name, $event); + } + + /** @param BeforeInstantiate $event */ + #[AsFoundryHook(EntityForEventListeners::class)] + public function beforeInstantiateWithFoundryAttribute(BeforeInstantiate $event): void + { + $event->parameters['name'] = "{$this->name($event->parameters['name'], $event)} with Foundry attribute"; + } + + /** @param AfterInstantiate $event */ + #[AsFoundryHook(EntityForEventListeners::class)] + public function afterInstantiateWithFoundryAttribute(AfterInstantiate $event): void + { + $event->object->name = "{$this->name($event->object->name, $event)} with Foundry attribute"; + } + + /** @param AfterPersist $event */ + #[AsFoundryHook(EntityForEventListeners::class)] + public function afterPersistWithFoundryAttribute(AfterPersist $event): void + { + $event->object->name = "{$this->name($event->object->name, $event)} with Foundry attribute"; + } + + /** @param BeforeInstantiate $event */ + #[AsFoundryHook()] + public function globalBeforeInstantiate(BeforeInstantiate $event): void + { + if (EntityForEventListeners::class !== $event->objectClass) { + return; + } + + $event->parameters['name'] = "{$this->name($event->parameters['name'], $event)} global"; + } + + /** @param AfterInstantiate $event */ + #[AsFoundryHook()] + public function globalAfterInstantiate(AfterInstantiate $event): void + { + if (!$event->object instanceof EntityForEventListeners) { + return; + } + + $event->object->name = "{$this->name($event->object->name, $event)} global"; + } + + /** @param AfterPersist $event */ + #[AsFoundryHook()] + public function globalAfterPersist(AfterPersist $event): void + { + if (!$event->object instanceof EntityForEventListeners) { + return; + } + + $event->object->name = "{$this->name($event->object->name, $event)} global"; + } + + private function name(string $name, Event $event): string // @phpstan-ignore missingType.generics + { + $eventName = (new \ReflectionClass($event))->getShortName(); + + return "{$name}\n{$eventName}"; + } +} diff --git a/tests/Fixture/Factories/ArrayFactory.php b/tests/Fixture/Factories/ArrayFactory.php index 39874e35d..c30f987ec 100644 --- a/tests/Fixture/Factories/ArrayFactory.php +++ b/tests/Fixture/Factories/ArrayFactory.php @@ -23,7 +23,7 @@ public function __construct(private ?UrlGeneratorInterface $router = null) { } - protected function defaults(): array|callable + protected function defaults(): array { return [ 'router' => (bool) $this->router, diff --git a/tests/Fixture/Factories/Entity/Address/CascadeAddressFactory.php b/tests/Fixture/Factories/Entity/Address/AddressFactory.php similarity index 67% rename from tests/Fixture/Factories/Entity/Address/CascadeAddressFactory.php rename to tests/Fixture/Factories/Entity/Address/AddressFactory.php index 25b362693..31c308f9a 100644 --- a/tests/Fixture/Factories/Entity/Address/CascadeAddressFactory.php +++ b/tests/Fixture/Factories/Entity/Address/AddressFactory.php @@ -12,21 +12,21 @@ namespace Zenstruck\Foundry\Tests\Fixture\Factories\Entity\Address; use Zenstruck\Foundry\Persistence\PersistentObjectFactory; -use Zenstruck\Foundry\Tests\Fixture\Entity\Address\CascadeAddress; +use Zenstruck\Foundry\Tests\Fixture\Entity\Address; /** * @author Kevin Bond * - * @extends PersistentObjectFactory + * @extends PersistentObjectFactory
*/ -final class CascadeAddressFactory extends PersistentObjectFactory +final class AddressFactory extends PersistentObjectFactory { public static function class(): string { - return CascadeAddress::class; + return Address::class; } - protected function defaults(): array|callable + protected function defaults(): array { return [ 'city' => self::faker()->city(), diff --git a/tests/Fixture/Factories/Entity/Address/ProxyAddressFactory.php b/tests/Fixture/Factories/Entity/Address/ProxyAddressFactory.php index c3dca085a..094f138ff 100644 --- a/tests/Fixture/Factories/Entity/Address/ProxyAddressFactory.php +++ b/tests/Fixture/Factories/Entity/Address/ProxyAddressFactory.php @@ -12,21 +12,21 @@ namespace Zenstruck\Foundry\Tests\Fixture\Factories\Entity\Address; use Zenstruck\Foundry\Persistence\PersistentProxyObjectFactory; -use Zenstruck\Foundry\Tests\Fixture\Entity\Address\StandardAddress; +use Zenstruck\Foundry\Tests\Fixture\Entity\Address; /** * @author Kevin Bond * - * @extends PersistentProxyObjectFactory + * @extends PersistentProxyObjectFactory
*/ final class ProxyAddressFactory extends PersistentProxyObjectFactory { public static function class(): string { - return StandardAddress::class; + return Address::class; } - protected function defaults(): array|callable + protected function defaults(): array { return [ 'city' => self::faker()->city(), diff --git a/tests/Fixture/Factories/Entity/Address/ProxyCascadeAddressFactory.php b/tests/Fixture/Factories/Entity/Address/ProxyCascadeAddressFactory.php deleted file mode 100644 index 5d1a82bb2..000000000 --- a/tests/Fixture/Factories/Entity/Address/ProxyCascadeAddressFactory.php +++ /dev/null @@ -1,35 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Zenstruck\Foundry\Tests\Fixture\Factories\Entity\Address; - -use Zenstruck\Foundry\Persistence\PersistentProxyObjectFactory; -use Zenstruck\Foundry\Tests\Fixture\Entity\Address\CascadeAddress; - -/** - * @author Nicolas PHILIPPE - * - * @extends PersistentProxyObjectFactory - */ -final class ProxyCascadeAddressFactory extends PersistentProxyObjectFactory -{ - public static function class(): string - { - return CascadeAddress::class; - } - - protected function defaults(): array|callable - { - return [ - 'city' => self::faker()->city(), - ]; - } -} diff --git a/tests/Fixture/Factories/Entity/Address/StandardAddressFactory.php b/tests/Fixture/Factories/Entity/Address/StandardAddressFactory.php deleted file mode 100644 index ead8a2c4f..000000000 --- a/tests/Fixture/Factories/Entity/Address/StandardAddressFactory.php +++ /dev/null @@ -1,35 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Zenstruck\Foundry\Tests\Fixture\Factories\Entity\Address; - -use Zenstruck\Foundry\Persistence\PersistentObjectFactory; -use Zenstruck\Foundry\Tests\Fixture\Entity\Address\StandardAddress; - -/** - * @author Kevin Bond - * - * @extends PersistentObjectFactory - */ -final class StandardAddressFactory extends PersistentObjectFactory -{ - public static function class(): string - { - return StandardAddress::class; - } - - protected function defaults(): array|callable - { - return [ - 'city' => self::faker()->city(), - ]; - } -} diff --git a/tests/Fixture/Factories/Entity/Category/CascadeCategoryFactory.php b/tests/Fixture/Factories/Entity/Category/CategoryFactory.php similarity index 67% rename from tests/Fixture/Factories/Entity/Category/CascadeCategoryFactory.php rename to tests/Fixture/Factories/Entity/Category/CategoryFactory.php index c50d705d3..d0846aa11 100644 --- a/tests/Fixture/Factories/Entity/Category/CascadeCategoryFactory.php +++ b/tests/Fixture/Factories/Entity/Category/CategoryFactory.php @@ -12,21 +12,21 @@ namespace Zenstruck\Foundry\Tests\Fixture\Factories\Entity\Category; use Zenstruck\Foundry\Persistence\PersistentObjectFactory; -use Zenstruck\Foundry\Tests\Fixture\Entity\Category\CascadeCategory; +use Zenstruck\Foundry\Tests\Fixture\Entity\Category; /** * @author Kevin Bond * - * @extends PersistentObjectFactory + * @extends PersistentObjectFactory */ -final class CascadeCategoryFactory extends PersistentObjectFactory +final class CategoryFactory extends PersistentObjectFactory { public static function class(): string { - return CascadeCategory::class; + return Category::class; } - protected function defaults(): array|callable + protected function defaults(): array { return [ 'name' => self::faker()->word(), diff --git a/tests/Fixture/Factories/Entity/Category/ProxyCascadeCategoryFactory.php b/tests/Fixture/Factories/Entity/Category/ProxyCascadeCategoryFactory.php deleted file mode 100644 index a3850718c..000000000 --- a/tests/Fixture/Factories/Entity/Category/ProxyCascadeCategoryFactory.php +++ /dev/null @@ -1,35 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Zenstruck\Foundry\Tests\Fixture\Factories\Entity\Category; - -use Zenstruck\Foundry\Persistence\PersistentProxyObjectFactory; -use Zenstruck\Foundry\Tests\Fixture\Entity\Category\CascadeCategory; - -/** - * @author Nicolas PHILIPPE - * - * @extends PersistentProxyObjectFactory - */ -final class ProxyCascadeCategoryFactory extends PersistentProxyObjectFactory -{ - public static function class(): string - { - return CascadeCategory::class; - } - - protected function defaults(): array|callable - { - return [ - 'name' => self::faker()->word(), - ]; - } -} diff --git a/tests/Fixture/Factories/Entity/Category/ProxyCategoryFactory.php b/tests/Fixture/Factories/Entity/Category/ProxyCategoryFactory.php index b5dcd115a..22016a573 100644 --- a/tests/Fixture/Factories/Entity/Category/ProxyCategoryFactory.php +++ b/tests/Fixture/Factories/Entity/Category/ProxyCategoryFactory.php @@ -12,21 +12,21 @@ namespace Zenstruck\Foundry\Tests\Fixture\Factories\Entity\Category; use Zenstruck\Foundry\Persistence\PersistentProxyObjectFactory; -use Zenstruck\Foundry\Tests\Fixture\Entity\Category\StandardCategory; +use Zenstruck\Foundry\Tests\Fixture\Entity\Category; /** * @author Kevin Bond * - * @extends PersistentProxyObjectFactory + * @extends PersistentProxyObjectFactory */ final class ProxyCategoryFactory extends PersistentProxyObjectFactory { public static function class(): string { - return StandardCategory::class; + return Category::class; } - protected function defaults(): array|callable + protected function defaults(): array { return [ 'name' => self::faker()->word(), diff --git a/tests/Fixture/Factories/Entity/Category/StandardCategoryFactory.php b/tests/Fixture/Factories/Entity/Category/StandardCategoryFactory.php deleted file mode 100644 index f6810626d..000000000 --- a/tests/Fixture/Factories/Entity/Category/StandardCategoryFactory.php +++ /dev/null @@ -1,35 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Zenstruck\Foundry\Tests\Fixture\Factories\Entity\Category; - -use Zenstruck\Foundry\Persistence\PersistentObjectFactory; -use Zenstruck\Foundry\Tests\Fixture\Entity\Category\StandardCategory; - -/** - * @author Kevin Bond - * - * @extends PersistentObjectFactory - */ -final class StandardCategoryFactory extends PersistentObjectFactory -{ - public static function class(): string - { - return StandardCategory::class; - } - - protected function defaults(): array|callable - { - return [ - 'name' => self::faker()->word(), - ]; - } -} diff --git a/tests/Fixture/Factories/Entity/Contact/CascadeContactFactory.php b/tests/Fixture/Factories/Entity/Contact/CascadeContactFactory.php deleted file mode 100644 index 3b98c679d..000000000 --- a/tests/Fixture/Factories/Entity/Contact/CascadeContactFactory.php +++ /dev/null @@ -1,38 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Zenstruck\Foundry\Tests\Fixture\Factories\Entity\Contact; - -use Zenstruck\Foundry\Persistence\PersistentObjectFactory; -use Zenstruck\Foundry\Tests\Fixture\Entity\Contact\CascadeContact; -use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\Address\CascadeAddressFactory; - -/** - * @author Kevin Bond - * - * @extends PersistentObjectFactory - */ -final class CascadeContactFactory extends PersistentObjectFactory -{ - public static function class(): string - { - return CascadeContact::class; - } - - protected function defaults(): array|callable - { - return [ - 'name' => self::faker()->word(), - // 'category' => CascadeCategoryFactory::new(), - 'address' => CascadeAddressFactory::new(), - ]; - } -} diff --git a/tests/Fixture/Factories/Entity/Contact/ChildContactFactory.php b/tests/Fixture/Factories/Entity/Contact/ChildContactFactory.php index 50a7f8667..baeb89e94 100644 --- a/tests/Fixture/Factories/Entity/Contact/ChildContactFactory.php +++ b/tests/Fixture/Factories/Entity/Contact/ChildContactFactory.php @@ -11,9 +11,9 @@ namespace Zenstruck\Foundry\Tests\Fixture\Factories\Entity\Contact; -use Zenstruck\Foundry\Tests\Fixture\Entity\Contact\ChildContact; +use Zenstruck\Foundry\Tests\Fixture\Entity\ChildContact; -final class ChildContactFactory extends StandardContactFactory +final class ChildContactFactory extends ContactFactory { public static function class(): string { diff --git a/tests/Fixture/Factories/Entity/Contact/StandardContactFactory.php b/tests/Fixture/Factories/Entity/Contact/ContactFactory.php similarity index 54% rename from tests/Fixture/Factories/Entity/Contact/StandardContactFactory.php rename to tests/Fixture/Factories/Entity/Contact/ContactFactory.php index 7990ce9d8..4058ffd23 100644 --- a/tests/Fixture/Factories/Entity/Contact/StandardContactFactory.php +++ b/tests/Fixture/Factories/Entity/Contact/ContactFactory.php @@ -12,26 +12,28 @@ namespace Zenstruck\Foundry\Tests\Fixture\Factories\Entity\Contact; use Zenstruck\Foundry\Persistence\PersistentObjectFactory; -use Zenstruck\Foundry\Tests\Fixture\Entity\Contact\StandardContact; -use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\Address\StandardAddressFactory; +use Zenstruck\Foundry\Tests\Fixture\Entity\Contact; +use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\Address\AddressFactory; +use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\Category\CategoryFactory; /** * @author Kevin Bond * - * @extends PersistentObjectFactory + * @extends PersistentObjectFactory */ -class StandardContactFactory extends PersistentObjectFactory +class ContactFactory extends PersistentObjectFactory { public static function class(): string { - return StandardContact::class; + return Contact::class; } - protected function defaults(): array|callable + protected function defaults(): array { return [ 'name' => self::faker()->word(), - 'address' => StandardAddressFactory::new(), + 'address' => AddressFactory::new(), + 'category' => CategoryFactory::new(), ]; } } diff --git a/tests/Fixture/Factories/Entity/Contact/ProxyCascadeContactFactory.php b/tests/Fixture/Factories/Entity/Contact/ProxyCascadeContactFactory.php deleted file mode 100644 index 41f93dab8..000000000 --- a/tests/Fixture/Factories/Entity/Contact/ProxyCascadeContactFactory.php +++ /dev/null @@ -1,38 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Zenstruck\Foundry\Tests\Fixture\Factories\Entity\Contact; - -use Zenstruck\Foundry\Persistence\PersistentProxyObjectFactory; -use Zenstruck\Foundry\Tests\Fixture\Entity\Contact\CascadeContact; -use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\Address\ProxyCascadeAddressFactory; - -/** - * @author Nicolas PHILIPPE - * - * @extends PersistentProxyObjectFactory - */ -final class ProxyCascadeContactFactory extends PersistentProxyObjectFactory -{ - public static function class(): string - { - return CascadeContact::class; - } - - protected function defaults(): array|callable - { - return [ - 'name' => self::faker()->word(), - // 'category' => ProxyCategoryFactory::new(), - 'address' => ProxyCascadeAddressFactory::new(), - ]; - } -} diff --git a/tests/Fixture/Factories/Entity/Contact/ProxyContactFactory.php b/tests/Fixture/Factories/Entity/Contact/ProxyContactFactory.php index 393714c2a..1871b8ccb 100644 --- a/tests/Fixture/Factories/Entity/Contact/ProxyContactFactory.php +++ b/tests/Fixture/Factories/Entity/Contact/ProxyContactFactory.php @@ -12,27 +12,27 @@ namespace Zenstruck\Foundry\Tests\Fixture\Factories\Entity\Contact; use Zenstruck\Foundry\Persistence\PersistentProxyObjectFactory; -use Zenstruck\Foundry\Tests\Fixture\Entity\Contact\StandardContact; +use Zenstruck\Foundry\Tests\Fixture\Entity\Contact; use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\Address\ProxyAddressFactory; use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\Category\ProxyCategoryFactory; /** * @author Kevin Bond * - * @extends PersistentProxyObjectFactory + * @extends PersistentProxyObjectFactory */ final class ProxyContactFactory extends PersistentProxyObjectFactory { public static function class(): string { - return StandardContact::class; + return Contact::class; } - protected function defaults(): array|callable + protected function defaults(): array { return [ 'name' => self::faker()->word(), - // 'category' => ProxyCategoryFactory::new(), + 'category' => ProxyCategoryFactory::new(), 'address' => ProxyAddressFactory::new(), ]; } diff --git a/tests/Fixture/Factories/Entity/EdgeCases/MultipleMandatoryRelationshipToSameEntity/InversedSideEntityFactory.php b/tests/Fixture/Factories/Entity/EdgeCases/MultipleMandatoryRelationshipToSameEntity/InversedSideEntityFactory.php index 2a18e0b10..a62a403d2 100644 --- a/tests/Fixture/Factories/Entity/EdgeCases/MultipleMandatoryRelationshipToSameEntity/InversedSideEntityFactory.php +++ b/tests/Fixture/Factories/Entity/EdgeCases/MultipleMandatoryRelationshipToSameEntity/InversedSideEntityFactory.php @@ -24,7 +24,7 @@ public static function class(): string return InversedSideEntity::class; } - protected function defaults(): array|callable + protected function defaults(): array { return []; } diff --git a/tests/Fixture/Factories/Entity/EdgeCases/MultipleMandatoryRelationshipToSameEntity/OwningSideEntityFactory.php b/tests/Fixture/Factories/Entity/EdgeCases/MultipleMandatoryRelationshipToSameEntity/OwningSideEntityFactory.php index b48436b52..a3cf8f2db 100644 --- a/tests/Fixture/Factories/Entity/EdgeCases/MultipleMandatoryRelationshipToSameEntity/OwningSideEntityFactory.php +++ b/tests/Fixture/Factories/Entity/EdgeCases/MultipleMandatoryRelationshipToSameEntity/OwningSideEntityFactory.php @@ -24,7 +24,7 @@ public static function class(): string return OwningSideEntity::class; } - protected function defaults(): array|callable + protected function defaults(): array { return [ 'main' => InversedSideEntityFactory::new(), diff --git a/tests/Fixture/Factories/Entity/Tag/ProxyCascadeTagFactory.php b/tests/Fixture/Factories/Entity/Tag/ProxyCascadeTagFactory.php deleted file mode 100644 index 8959920f0..000000000 --- a/tests/Fixture/Factories/Entity/Tag/ProxyCascadeTagFactory.php +++ /dev/null @@ -1,35 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Zenstruck\Foundry\Tests\Fixture\Factories\Entity\Tag; - -use Zenstruck\Foundry\Persistence\PersistentProxyObjectFactory; -use Zenstruck\Foundry\Tests\Fixture\Entity\Tag\CascadeTag; - -/** - * @author Nicolas PHILIPPE - * - * @extends PersistentProxyObjectFactory - */ -final class ProxyCascadeTagFactory extends PersistentProxyObjectFactory -{ - public static function class(): string - { - return CascadeTag::class; - } - - protected function defaults(): array|callable - { - return [ - 'name' => self::faker()->word(), - ]; - } -} diff --git a/tests/Fixture/Factories/Entity/Tag/ProxyTagFactory.php b/tests/Fixture/Factories/Entity/Tag/ProxyTagFactory.php index a12f4aa64..ca73bd963 100644 --- a/tests/Fixture/Factories/Entity/Tag/ProxyTagFactory.php +++ b/tests/Fixture/Factories/Entity/Tag/ProxyTagFactory.php @@ -12,21 +12,21 @@ namespace Zenstruck\Foundry\Tests\Fixture\Factories\Entity\Tag; use Zenstruck\Foundry\Persistence\PersistentProxyObjectFactory; -use Zenstruck\Foundry\Tests\Fixture\Entity\Tag\StandardTag; +use Zenstruck\Foundry\Tests\Fixture\Entity\Tag; /** * @author Kevin Bond * - * @extends PersistentProxyObjectFactory + * @extends PersistentProxyObjectFactory */ final class ProxyTagFactory extends PersistentProxyObjectFactory { public static function class(): string { - return StandardTag::class; + return Tag::class; } - protected function defaults(): array|callable + protected function defaults(): array { return [ 'name' => self::faker()->word(), diff --git a/tests/Fixture/Factories/Entity/Tag/StandardTagFactory.php b/tests/Fixture/Factories/Entity/Tag/StandardTagFactory.php deleted file mode 100644 index e96ead5b9..000000000 --- a/tests/Fixture/Factories/Entity/Tag/StandardTagFactory.php +++ /dev/null @@ -1,35 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Zenstruck\Foundry\Tests\Fixture\Factories\Entity\Tag; - -use Zenstruck\Foundry\Persistence\PersistentObjectFactory; -use Zenstruck\Foundry\Tests\Fixture\Entity\Tag\StandardTag; - -/** - * @author Kevin Bond - * - * @extends PersistentObjectFactory - */ -final class StandardTagFactory extends PersistentObjectFactory -{ - public static function class(): string - { - return StandardTag::class; - } - - protected function defaults(): array|callable - { - return [ - 'name' => self::faker()->word(), - ]; - } -} diff --git a/tests/Fixture/Factories/Entity/Tag/CascadeTagFactory.php b/tests/Fixture/Factories/Entity/Tag/TagFactory.php similarity index 69% rename from tests/Fixture/Factories/Entity/Tag/CascadeTagFactory.php rename to tests/Fixture/Factories/Entity/Tag/TagFactory.php index 5b4586454..8f4854754 100644 --- a/tests/Fixture/Factories/Entity/Tag/CascadeTagFactory.php +++ b/tests/Fixture/Factories/Entity/Tag/TagFactory.php @@ -12,21 +12,21 @@ namespace Zenstruck\Foundry\Tests\Fixture\Factories\Entity\Tag; use Zenstruck\Foundry\Persistence\PersistentObjectFactory; -use Zenstruck\Foundry\Tests\Fixture\Entity\Tag\CascadeTag; +use Zenstruck\Foundry\Tests\Fixture\Entity\Tag; /** * @author Kevin Bond * - * @extends PersistentObjectFactory + * @extends PersistentObjectFactory */ -final class CascadeTagFactory extends PersistentObjectFactory +final class TagFactory extends PersistentObjectFactory { public static function class(): string { - return CascadeTag::class; + return Tag::class; } - protected function defaults(): array|callable + protected function defaults(): array { return [ 'name' => self::faker()->word(), diff --git a/tests/Fixture/Factories/GenericModelFactory.php b/tests/Fixture/Factories/GenericModelFactory.php index 0cf14fda4..3b39110d6 100644 --- a/tests/Fixture/Factories/GenericModelFactory.php +++ b/tests/Fixture/Factories/GenericModelFactory.php @@ -21,7 +21,7 @@ */ abstract class GenericModelFactory extends PersistentObjectFactory { - protected function defaults(): array|callable + protected function defaults(): array { return [ 'prop1' => 'default1', diff --git a/tests/Fixture/Factories/GenericProxyModelFactory.php b/tests/Fixture/Factories/GenericProxyModelFactory.php index 0b8c7fd18..2db6f353e 100644 --- a/tests/Fixture/Factories/GenericProxyModelFactory.php +++ b/tests/Fixture/Factories/GenericProxyModelFactory.php @@ -21,7 +21,7 @@ */ abstract class GenericProxyModelFactory extends PersistentProxyObjectFactory { - protected function defaults(): array|callable + protected function defaults(): array { return [ 'prop1' => 'default1', diff --git a/tests/Fixture/Factories/Object1Factory.php b/tests/Fixture/Factories/Object1Factory.php index 0e8ab6dd0..2da868fa7 100644 --- a/tests/Fixture/Factories/Object1Factory.php +++ b/tests/Fixture/Factories/Object1Factory.php @@ -31,7 +31,7 @@ public static function class(): string return Object1::class; } - protected function defaults(): array|callable + protected function defaults(): array { return [ 'prop1' => $this->router ? 'router' : 'value1', diff --git a/tests/Fixture/Factories/Object2Factory.php b/tests/Fixture/Factories/Object2Factory.php index c197506e2..79fb025ba 100644 --- a/tests/Fixture/Factories/Object2Factory.php +++ b/tests/Fixture/Factories/Object2Factory.php @@ -26,7 +26,7 @@ public static function class(): string return Object2::class; } - protected function defaults(): array|callable + protected function defaults(): array { return [ 'object' => Object1Factory::new(), diff --git a/tests/Fixture/Factories/WithHooksInInitializeFactory.php b/tests/Fixture/Factories/WithHooksInInitializeFactory.php new file mode 100644 index 000000000..f52510a97 --- /dev/null +++ b/tests/Fixture/Factories/WithHooksInInitializeFactory.php @@ -0,0 +1,57 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Tests\Fixture\Factories; + +use Zenstruck\Foundry\Persistence\PersistentObjectFactory; +use Zenstruck\Foundry\Tests\Fixture\Entity\Address; + +/** + * @author Nicolas Philippe + * @extends PersistentObjectFactory
+ */ +final class WithHooksInInitializeFactory extends PersistentObjectFactory +{ + public static function class(): string + { + return Address::class; + } + + protected function defaults(): array + { + return [ + 'city' => self::faker()->city(), + ]; + } + + protected function initialize(): static + { + return $this + ->beforeInstantiate( + function(array $parameters, string $class, WithHooksInInitializeFactory $factory) { + if (!$factory->isPersisting()) { + $parameters['city'] = 'beforeInstantiate'; + } + + return $parameters; + } + ) + ->afterInstantiate( + function(Address $object, array $parameters, WithHooksInInitializeFactory $factory) { + if (!$factory->isPersisting()) { + $object->setCity("{$object->getCity()} - afterInstantiate"); + } + } + ); + } +} diff --git a/tests/Fixture/FoundryTestKernel.php b/tests/Fixture/FoundryTestKernel.php new file mode 100644 index 000000000..a649dff4e --- /dev/null +++ b/tests/Fixture/FoundryTestKernel.php @@ -0,0 +1,162 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Tests\Fixture; + +use DAMA\DoctrineTestBundle\DAMADoctrineTestBundle; +use Doctrine\Bundle\DoctrineBundle\DoctrineBundle; +use Doctrine\Bundle\MongoDBBundle\DoctrineMongoDBBundle; +use Psr\Log\NullLogger; +use Symfony\Bundle\FrameworkBundle\FrameworkBundle; +use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait; +use Symfony\Component\Config\Loader\LoaderInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\HttpKernel\Kernel; +use Zenstruck\Foundry\Persistence\PersistenceManager; +use Zenstruck\Foundry\Tests\Fixture\DoctrineCascadeRelationship\ChangeCascadePersistOnLoadClassMetadataListener; +use Zenstruck\Foundry\ZenstruckFoundryBundle; + +/** + * @author Nicolas PHILIPPE + */ +abstract class FoundryTestKernel extends Kernel +{ + use MicroKernelTrait; + + public function registerBundles(): iterable + { + yield new FrameworkBundle(); + + if (self::hasORM()) { + yield new DoctrineBundle(); + } + + if (self::hasMongo()) { + yield new DoctrineMongoDBBundle(); + } + + yield new ZenstruckFoundryBundle(); + + if (self::usesDamaDoctrineTestBundle()) { + yield new DAMADoctrineTestBundle(); + } + } + + public static function hasORM(): bool + { + return (bool) \getenv('DATABASE_URL'); + } + + public static function hasMongo(): bool + { + return (bool) \getenv('MONGO_URL'); + } + + public static function usesMigrations(): bool + { + return 'migrate' === \getenv('DATABASE_RESET_MODE'); + } + + public static function usesDamaDoctrineTestBundle(): bool + { + return (bool) \getenv('USE_DAMA_DOCTRINE_TEST_BUNDLE'); + } + + protected function configureContainer(ContainerBuilder $c, LoaderInterface $loader): void + { + $c->loadFromExtension('framework', [ + 'http_method_override' => false, + 'secret' => 'S3CRET', + 'router' => ['utf8' => true], + 'test' => true, + ]); + + if (self::hasORM()) { + $c->loadFromExtension('doctrine', [ + 'dbal' => ['url' => '%env(resolve:DATABASE_URL)%', 'use_savepoints' => true], + 'orm' => [ + 'auto_generate_proxy_classes' => true, + 'auto_mapping' => true, + 'mappings' => [ + 'Entity' => [ + 'is_bundle' => false, + 'type' => 'attribute', + 'dir' => '%kernel.project_dir%/tests/Fixture/Entity', + 'prefix' => 'Zenstruck\Foundry\Tests\Fixture\Entity', + 'alias' => 'Entity', + ], + 'Model' => [ + 'is_bundle' => false, + 'type' => 'attribute', + 'dir' => '%kernel.project_dir%/tests/Fixture/Model', + 'prefix' => 'Zenstruck\Foundry\Tests\Fixture\Model', + 'alias' => 'Model', + ], + + // postgres acts weirdly with multiple schemas + // @see https://github.com/doctrine/DoctrineBundle/issues/548 + ...(\str_starts_with(\getenv('DATABASE_URL') ?: '', 'postgresql') + ? [ + 'EntityInAnotherSchema' => [ + 'is_bundle' => false, + 'type' => 'attribute', + 'dir' => '%kernel.project_dir%/tests/Fixture/EntityInAnotherSchema', + 'prefix' => 'Zenstruck\Foundry\Tests\Fixture\EntityInAnotherSchema', + 'alias' => 'Migrate', + ], + ] + : [] + ), + ], + 'controller_resolver' => ['auto_mapping' => false], + ], + ]); + + $c->register(ChangeCascadePersistOnLoadClassMetadataListener::class) + ->setAutowired(true) + ->setAutoconfigured(true); + $c->setAlias(PersistenceManager::class, '.zenstruck_foundry.persistence_manager') + ->setPublic(true); + } + + if (self::hasMongo()) { + $c->loadFromExtension('doctrine_mongodb', [ + 'connections' => [ + 'default' => ['server' => '%env(resolve:MONGO_URL)%'], + ], + 'default_database' => 'mongo', + 'document_managers' => [ + 'default' => [ + 'auto_mapping' => true, + 'mappings' => [ + 'Document' => [ + 'is_bundle' => false, + 'type' => 'attribute', + 'dir' => '%kernel.project_dir%/tests/Fixture/Document', + 'prefix' => 'Zenstruck\Foundry\Tests\Fixture\Document', + 'alias' => 'Document', + ], + 'Model' => [ + 'is_bundle' => false, + 'type' => 'attribute', + 'dir' => '%kernel.project_dir%/tests/Fixture/Model', + 'prefix' => 'Zenstruck\Foundry\Tests\Fixture\Model', + 'alias' => 'Model', + ], + ], + ], + ], + ]); + } + + $c->register('logger', NullLogger::class); + } +} diff --git a/tests/Fixture/InMemory/InMemoryAddressRepository.php b/tests/Fixture/InMemory/InMemoryAddressRepository.php new file mode 100644 index 000000000..940265cf1 --- /dev/null +++ b/tests/Fixture/InMemory/InMemoryAddressRepository.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\Tests\Fixture\InMemory; + +use Zenstruck\Foundry\InMemory\InMemoryRepository; +use Zenstruck\Foundry\InMemory\InMemoryRepositoryTrait; +use Zenstruck\Foundry\Tests\Fixture\Entity\Address; + +/** + * @implements InMemoryRepository
+ */ +final class InMemoryAddressRepository implements InMemoryRepository +{ + /** @use InMemoryRepositoryTrait
*/ + use InMemoryRepositoryTrait; + + public static function _class(): string + { + return Address::class; + } +} diff --git a/tests/Fixture/InMemory/InMemoryContactRepository.php b/tests/Fixture/InMemory/InMemoryContactRepository.php new file mode 100644 index 000000000..f3014b5fb --- /dev/null +++ b/tests/Fixture/InMemory/InMemoryContactRepository.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\Tests\Fixture\InMemory; + +use Zenstruck\Foundry\InMemory\InMemoryRepository; +use Zenstruck\Foundry\InMemory\InMemoryRepositoryTrait; +use Zenstruck\Foundry\Tests\Fixture\Entity\Contact; + +/** + * @implements InMemoryRepository + */ +final class InMemoryContactRepository implements InMemoryRepository +{ + /** @use InMemoryRepositoryTrait */ + use InMemoryRepositoryTrait; + + public static function _class(): string + { + return Contact::class; + } +} diff --git a/tests/Fixture/Maker/expected/can_create_factory.php b/tests/Fixture/Maker/expected/can_create_factory.php index 8fc12b2fb..0c948121f 100644 --- a/tests/Fixture/Maker/expected/can_create_factory.php +++ b/tests/Fixture/Maker/expected/can_create_factory.php @@ -9,15 +9,15 @@ * file that was distributed with this source code. */ -namespace App\Factory\Category; +namespace App\Factory; use Zenstruck\Foundry\Persistence\PersistentProxyObjectFactory; -use Zenstruck\Foundry\Tests\Fixture\Entity\Category\StandardCategory; +use Zenstruck\Foundry\Tests\Fixture\Entity\Category; /** - * @extends PersistentProxyObjectFactory + * @extends PersistentProxyObjectFactory */ -final class StandardCategoryFactory extends PersistentProxyObjectFactory +final class CategoryFactory extends PersistentProxyObjectFactory { /** * @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#factories-as-services @@ -30,7 +30,7 @@ public function __construct() public static function class(): string { - return StandardCategory::class; + return Category::class; } /** @@ -51,7 +51,7 @@ protected function defaults(): array|callable protected function initialize(): static { return $this - // ->afterInstantiate(function(StandardCategory $standardCategory): void {}) + // ->afterInstantiate(function(Category $category): void {}) ; } } diff --git a/tests/Fixture/Maker/expected/can_create_factory_for_entity_with_repository.php b/tests/Fixture/Maker/expected/can_create_factory_for_entity_with_repository.php index f92b522a5..f9e415086 100644 --- a/tests/Fixture/Maker/expected/can_create_factory_for_entity_with_repository.php +++ b/tests/Fixture/Maker/expected/can_create_factory_for_entity_with_repository.php @@ -25,8 +25,8 @@ * @method static GenericEntity|Proxy createOne(array $attributes = []) * @method static GenericEntity|Proxy find(object|array|mixed $criteria) * @method static GenericEntity|Proxy findOrCreate(array $attributes) - * @method static GenericEntity|Proxy first(string $sortedField = 'id') - * @method static GenericEntity|Proxy last(string $sortedField = 'id') + * @method static GenericEntity|Proxy first(string $sortBy = 'id') + * @method static GenericEntity|Proxy last(string $sortBy = 'id') * @method static GenericEntity|Proxy random(array $attributes = []) * @method static GenericEntity|Proxy randomOrCreate(array $attributes = []) * @method static GenericEntityRepository|ProxyRepositoryDecorator repository() @@ -41,11 +41,11 @@ * @phpstan-method static GenericEntity&Proxy createOne(array $attributes = []) * @phpstan-method static GenericEntity&Proxy find(object|array|mixed $criteria) * @phpstan-method static GenericEntity&Proxy findOrCreate(array $attributes) - * @phpstan-method static GenericEntity&Proxy first(string $sortedField = 'id') - * @phpstan-method static GenericEntity&Proxy last(string $sortedField = 'id') + * @phpstan-method static GenericEntity&Proxy first(string $sortBy = 'id') + * @phpstan-method static GenericEntity&Proxy last(string $sortBy = 'id') * @phpstan-method static GenericEntity&Proxy random(array $attributes = []) * @phpstan-method static GenericEntity&Proxy randomOrCreate(array $attributes = []) - * @phpstan-method static ProxyRepositoryDecorator repository() + * @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) diff --git a/tests/Fixture/Maker/expected/can_create_factory_interactively.php b/tests/Fixture/Maker/expected/can_create_factory_interactively.php index fe606ae39..4dc298fe3 100644 --- a/tests/Fixture/Maker/expected/can_create_factory_interactively.php +++ b/tests/Fixture/Maker/expected/can_create_factory_interactively.php @@ -9,16 +9,15 @@ * file that was distributed with this source code. */ -namespace App\Factory\Contact; +namespace App\Factory; -use App\Factory\Address\StandardAddressFactory; use Zenstruck\Foundry\Persistence\PersistentProxyObjectFactory; -use Zenstruck\Foundry\Tests\Fixture\Entity\Contact\StandardContact; +use Zenstruck\Foundry\Tests\Fixture\Entity\Contact; /** - * @extends PersistentProxyObjectFactory + * @extends PersistentProxyObjectFactory */ -final class StandardContactFactory extends PersistentProxyObjectFactory +final class ContactFactory extends PersistentProxyObjectFactory { /** * @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#factories-as-services @@ -31,7 +30,7 @@ public function __construct() public static function class(): string { - return StandardContact::class; + return Contact::class; } /** @@ -42,7 +41,7 @@ public static function class(): string protected function defaults(): array|callable { return [ - 'address' => StandardAddressFactory::new(), + 'address' => AddressFactory::new(), 'name' => self::faker()->text(255), ]; } @@ -53,7 +52,7 @@ protected function defaults(): array|callable protected function initialize(): static { return $this - // ->afterInstantiate(function(StandardContact $standardContact): void {}) + // ->afterInstantiate(function(Contact $contact): void {}) ; } } diff --git a/tests/Fixture/Maker/expected/can_create_factory_with_static_analysis_annotations_with_data_set_phpstan.php b/tests/Fixture/Maker/expected/can_create_factory_with_static_analysis_annotations_with_data_set_phpstan.php index 679a6db58..57d0f770b 100644 --- a/tests/Fixture/Maker/expected/can_create_factory_with_static_analysis_annotations_with_data_set_phpstan.php +++ b/tests/Fixture/Maker/expected/can_create_factory_with_static_analysis_annotations_with_data_set_phpstan.php @@ -9,50 +9,50 @@ * file that was distributed with this source code. */ -namespace App\Tests\Factory\Category; +namespace App\Tests\Factory; use Doctrine\ORM\EntityRepository; use Zenstruck\Foundry\Persistence\PersistentProxyObjectFactory; use Zenstruck\Foundry\Persistence\Proxy; use Zenstruck\Foundry\Persistence\ProxyRepositoryDecorator; -use Zenstruck\Foundry\Tests\Fixture\Entity\Category\StandardCategory; +use Zenstruck\Foundry\Tests\Fixture\Entity\Category; /** - * @extends PersistentProxyObjectFactory + * @extends PersistentProxyObjectFactory * - * @method StandardCategory|Proxy create(array|callable $attributes = []) - * @method static StandardCategory|Proxy createOne(array $attributes = []) - * @method static StandardCategory|Proxy find(object|array|mixed $criteria) - * @method static StandardCategory|Proxy findOrCreate(array $attributes) - * @method static StandardCategory|Proxy first(string $sortedField = 'id') - * @method static StandardCategory|Proxy last(string $sortedField = 'id') - * @method static StandardCategory|Proxy random(array $attributes = []) - * @method static StandardCategory|Proxy randomOrCreate(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 $sortBy = 'id') + * @method static Category|Proxy last(string $sortBy = 'id') + * @method static Category|Proxy random(array $attributes = []) + * @method static Category|Proxy randomOrCreate(array $attributes = []) * @method static EntityRepository|ProxyRepositoryDecorator repository() - * @method static StandardCategory[]|Proxy[] all() - * @method static StandardCategory[]|Proxy[] createMany(int $number, array|callable $attributes = []) - * @method static StandardCategory[]|Proxy[] createSequence(iterable|callable $sequence) - * @method static StandardCategory[]|Proxy[] findBy(array $attributes) - * @method static StandardCategory[]|Proxy[] randomRange(int $min, int $max, array $attributes = []) - * @method static StandardCategory[]|Proxy[] randomSet(int $number, array $attributes = []) + * @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 StandardCategory&Proxy create(array|callable $attributes = []) - * @phpstan-method static StandardCategory&Proxy createOne(array $attributes = []) - * @phpstan-method static StandardCategory&Proxy find(object|array|mixed $criteria) - * @phpstan-method static StandardCategory&Proxy findOrCreate(array $attributes) - * @phpstan-method static StandardCategory&Proxy first(string $sortedField = 'id') - * @phpstan-method static StandardCategory&Proxy last(string $sortedField = 'id') - * @phpstan-method static StandardCategory&Proxy random(array $attributes = []) - * @phpstan-method static StandardCategory&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 = []) + * @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 $sortBy = 'id') + * @phpstan-method static Category&Proxy last(string $sortBy = '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 StandardCategoryFactory extends PersistentProxyObjectFactory +final class CategoryFactory extends PersistentProxyObjectFactory { /** * @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#factories-as-services @@ -65,7 +65,7 @@ public function __construct() public static function class(): string { - return StandardCategory::class; + return Category::class; } /** @@ -86,7 +86,7 @@ protected function defaults(): array|callable protected function initialize(): static { return $this - // ->afterInstantiate(function(StandardCategory $standardCategory): void {}) + // ->afterInstantiate(function(Category $category): void {}) ; } } diff --git a/tests/Fixture/Maker/expected/can_create_factory_with_static_analysis_annotations_with_data_set_psalm.php b/tests/Fixture/Maker/expected/can_create_factory_with_static_analysis_annotations_with_data_set_psalm.php index 80b871e2e..7d368c359 100644 --- a/tests/Fixture/Maker/expected/can_create_factory_with_static_analysis_annotations_with_data_set_psalm.php +++ b/tests/Fixture/Maker/expected/can_create_factory_with_static_analysis_annotations_with_data_set_psalm.php @@ -9,50 +9,50 @@ * file that was distributed with this source code. */ -namespace App\Tests\Factory\Category; +namespace App\Tests\Factory; use Doctrine\ORM\EntityRepository; use Zenstruck\Foundry\Persistence\PersistentProxyObjectFactory; use Zenstruck\Foundry\Persistence\Proxy; use Zenstruck\Foundry\Persistence\ProxyRepositoryDecorator; -use Zenstruck\Foundry\Tests\Fixture\Entity\Category\StandardCategory; +use Zenstruck\Foundry\Tests\Fixture\Entity\Category; /** - * @extends PersistentProxyObjectFactory + * @extends PersistentProxyObjectFactory * - * @method StandardCategory|Proxy create(array|callable $attributes = []) - * @method static StandardCategory|Proxy createOne(array $attributes = []) - * @method static StandardCategory|Proxy find(object|array|mixed $criteria) - * @method static StandardCategory|Proxy findOrCreate(array $attributes) - * @method static StandardCategory|Proxy first(string $sortedField = 'id') - * @method static StandardCategory|Proxy last(string $sortedField = 'id') - * @method static StandardCategory|Proxy random(array $attributes = []) - * @method static StandardCategory|Proxy randomOrCreate(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 $sortBy = 'id') + * @method static Category|Proxy last(string $sortBy = 'id') + * @method static Category|Proxy random(array $attributes = []) + * @method static Category|Proxy randomOrCreate(array $attributes = []) * @method static EntityRepository|ProxyRepositoryDecorator repository() - * @method static StandardCategory[]|Proxy[] all() - * @method static StandardCategory[]|Proxy[] createMany(int $number, array|callable $attributes = []) - * @method static StandardCategory[]|Proxy[] createSequence(iterable|callable $sequence) - * @method static StandardCategory[]|Proxy[] findBy(array $attributes) - * @method static StandardCategory[]|Proxy[] randomRange(int $min, int $max, array $attributes = []) - * @method static StandardCategory[]|Proxy[] randomSet(int $number, array $attributes = []) + * @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 StandardCategory&Proxy create(array|callable $attributes = []) - * @psalm-method static StandardCategory&Proxy createOne(array $attributes = []) - * @psalm-method static StandardCategory&Proxy find(object|array|mixed $criteria) - * @psalm-method static StandardCategory&Proxy findOrCreate(array $attributes) - * @psalm-method static StandardCategory&Proxy first(string $sortedField = 'id') - * @psalm-method static StandardCategory&Proxy last(string $sortedField = 'id') - * @psalm-method static StandardCategory&Proxy random(array $attributes = []) - * @psalm-method static StandardCategory&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 = []) + * @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 $sortBy = 'id') + * @psalm-method static Category&Proxy last(string $sortBy = '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 StandardCategoryFactory extends PersistentProxyObjectFactory +final class CategoryFactory extends PersistentProxyObjectFactory { /** * @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#factories-as-services @@ -65,7 +65,7 @@ public function __construct() public static function class(): string { - return StandardCategory::class; + return Category::class; } /** @@ -86,7 +86,7 @@ protected function defaults(): array|callable protected function initialize(): static { return $this - // ->afterInstantiate(function(StandardCategory $standardCategory): void {}) + // ->afterInstantiate(function(Category $category): void {}) ; } } diff --git a/tests/Fixture/Maker/expected/does_force_initialization_of_non_settable_with_always_force.php b/tests/Fixture/Maker/expected/does_force_initialization_of_non_settable_with_always_force.php new file mode 100644 index 000000000..322d270c5 --- /dev/null +++ b/tests/Fixture/Maker/expected/does_force_initialization_of_non_settable_with_always_force.php @@ -0,0 +1,59 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace App\Factory; + +use Zenstruck\Foundry\ObjectFactory; +use Zenstruck\Foundry\Tests\Fixture\ObjectWithNonWriteable; + +/** + * @extends ObjectFactory + */ +final class ObjectWithNonWriteableFactory extends ObjectFactory +{ + /** + * @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#factories-as-services + * + * @todo inject services if required + */ + public function __construct() + { + } + + public static function class(): string + { + return ObjectWithNonWriteable::class; + } + + /** + * @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#model-factories + * + * @todo add your default values here + */ + protected function defaults(): array|callable + { + return [ + 'bar' => self::faker()->sentence(), + 'baz' => self::faker()->sentence(), + 'foo' => self::faker()->sentence(), + ]; + } + + /** + * @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#initialization + */ + protected function initialize(): static + { + return $this + // ->afterInstantiate(function(ObjectWithNonWriteable $objectWithNonWriteable): void {}) + ; + } +} diff --git a/tests/Fixture/Maker/expected/does_not_initialize_non_settable.php b/tests/Fixture/Maker/expected/does_not_initialize_non_settable.php new file mode 100644 index 000000000..8d7b7948a --- /dev/null +++ b/tests/Fixture/Maker/expected/does_not_initialize_non_settable.php @@ -0,0 +1,58 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace App\Factory; + +use Zenstruck\Foundry\ObjectFactory; +use Zenstruck\Foundry\Tests\Fixture\ObjectWithNonWriteable; + +/** + * @extends ObjectFactory + */ +final class ObjectWithNonWriteableFactory extends ObjectFactory +{ + /** + * @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#factories-as-services + * + * @todo inject services if required + */ + public function __construct() + { + } + + public static function class(): string + { + return ObjectWithNonWriteable::class; + } + + /** + * @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#model-factories + * + * @todo add your default values here + */ + protected function defaults(): array|callable + { + return [ + 'baz' => self::faker()->sentence(), + 'foo' => self::faker()->sentence(), + ]; + } + + /** + * @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#initialization + */ + protected function initialize(): static + { + return $this + // ->afterInstantiate(function(ObjectWithNonWriteable $objectWithNonWriteable): void {}) + ; + } +} diff --git a/tests/Fixture/ObjectWithEnum.php b/tests/Fixture/ObjectWithEnum.php index e40a306a6..5f70aaddf 100644 --- a/tests/Fixture/ObjectWithEnum.php +++ b/tests/Fixture/ObjectWithEnum.php @@ -1,12 +1,20 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Zenstruck\Foundry\Tests\Fixture; final class ObjectWithEnum { public function __construct( - public readonly SomeEnum $someEnum - ) - { + public readonly SomeEnum $someEnum, + ) { } } diff --git a/tests/Fixture/ObjectWithNonWriteable.php b/tests/Fixture/ObjectWithNonWriteable.php new file mode 100644 index 000000000..3a0ef8a90 --- /dev/null +++ b/tests/Fixture/ObjectWithNonWriteable.php @@ -0,0 +1,42 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Tests\Fixture; + +final class ObjectWithNonWriteable +{ + private string $bar; + // @phpstan-ignore-next-line We do not want assign a default value to this so the factory sets one + private string $baz; + + public function __construct( + public readonly string $foo, + ) { + $this->bar = 'bar'; + } + + public function getBaz(): string + { + return $this->baz; + } + + public function setBaz(string $baz): self + { + $this->baz = $baz; + + return $this; + } + + public function getBar(): string + { + return $this->bar; + } +} diff --git a/tests/Fixture/ResetDatabase/MongoResetterDecorator.php b/tests/Fixture/ResetDatabase/MongoResetterDecorator.php new file mode 100644 index 000000000..05ab0a822 --- /dev/null +++ b/tests/Fixture/ResetDatabase/MongoResetterDecorator.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Tests\Fixture\ResetDatabase; + +use Symfony\Component\DependencyInjection\Attribute\AsDecorator; +use Symfony\Component\HttpKernel\KernelInterface; +use Zenstruck\Foundry\Mongo\MongoResetter; + +#[AsDecorator(MongoResetter::class)] +final class MongoResetterDecorator implements MongoResetter +{ + public static bool $calledBeforeEachTest = false; + + public function __construct( + private MongoResetter $decorated, + ) { + } + + public function resetBeforeEachTest(KernelInterface $kernel): void + { + self::$calledBeforeEachTest = true; + + $this->decorated->resetBeforeEachTest($kernel); + } +} diff --git a/tests/Fixture/ResetDatabase/OrmResetterDecorator.php b/tests/Fixture/ResetDatabase/OrmResetterDecorator.php new file mode 100644 index 000000000..0bd836458 --- /dev/null +++ b/tests/Fixture/ResetDatabase/OrmResetterDecorator.php @@ -0,0 +1,50 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Tests\Fixture\ResetDatabase; + +use Symfony\Component\DependencyInjection\Attribute\AsDecorator; +use Symfony\Component\HttpKernel\KernelInterface; +use Zenstruck\Foundry\ORM\ResetDatabase\OrmResetter; + +#[AsDecorator(OrmResetter::class)] +final class OrmResetterDecorator implements OrmResetter +{ + public static bool $calledBeforeFirstTest = false; + public static bool $calledBeforeEachTest = false; + + public function __construct( + private OrmResetter $decorated, + ) { + } + + public function resetBeforeFirstTest(KernelInterface $kernel): void + { + self::$calledBeforeFirstTest = true; + + $this->decorated->resetBeforeFirstTest($kernel); + } + + public function resetBeforeEachTest(KernelInterface $kernel): void + { + self::$calledBeforeEachTest = true; + + $this->decorated->resetBeforeEachTest($kernel); + } + + public static function reset(): void + { + self::$calledBeforeFirstTest = false; + self::$calledBeforeEachTest = false; + } +} diff --git a/tests/Fixture/ResetDatabase/ResetDatabaseTestKernel.php b/tests/Fixture/ResetDatabase/ResetDatabaseTestKernel.php new file mode 100644 index 000000000..f4d28b26f --- /dev/null +++ b/tests/Fixture/ResetDatabase/ResetDatabaseTestKernel.php @@ -0,0 +1,75 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Tests\Fixture\ResetDatabase; + +use Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle; +use Symfony\Component\Config\Loader\LoaderInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Zenstruck\Foundry\ORM\ResetDatabase\ResetDatabaseMode; +use Zenstruck\Foundry\Tests\Fixture\FoundryTestKernel; +use Zenstruck\Foundry\Tests\Fixture\Stories\GlobalInvokableService; +use Zenstruck\Foundry\Tests\Fixture\Stories\GlobalStory; + +/** + * @author Nicolas PHILIPPE + */ +final class ResetDatabaseTestKernel extends FoundryTestKernel +{ + public function registerBundles(): iterable + { + yield from parent::registerBundles(); + + if (FoundryTestKernel::usesMigrations()) { + yield new DoctrineMigrationsBundle(); + } + } + + protected function configureContainer(ContainerBuilder $c, LoaderInterface $loader): void + { + parent::configureContainer($c, $loader); + + $c->loadFromExtension('zenstruck_foundry', [ + 'global_state' => [ + GlobalStory::class, + GlobalInvokableService::class, + ], + 'orm' => [ + 'reset' => FoundryTestKernel::usesMigrations() + ? [ + 'mode' => ResetDatabaseMode::MIGRATE, + 'migrations' => [ + 'configurations' => ($configFile = \getenv('MIGRATION_CONFIGURATION_FILE')) ? [$configFile] : [], + ], + ] + : ['mode' => ResetDatabaseMode::SCHEMA], + ], + ]); + + if (FoundryTestKernel::usesMigrations() && !\getenv('MIGRATION_CONFIGURATION_FILE')) { + // if no configuration file was given in Foundry's config, let's use the main one as default. + $c->loadFromExtension( + 'doctrine_migrations', + include __DIR__.'/migration-configs/migration-configuration.php' + ); + } + + $c->register(GlobalInvokableService::class); + + if (self::hasORM()) { + $c->register(OrmResetterDecorator::class)->setAutowired(true)->setAutoconfigured(true); + } + + if (self::hasMongo()) { + $c->register(MongoResetterDecorator::class)->setAutowired(true)->setAutoconfigured(true); + } + } +} diff --git a/tests/Fixture/ResetDatabase/migration-configs/migration-configuration-transactional.php b/tests/Fixture/ResetDatabase/migration-configs/migration-configuration-transactional.php new file mode 100644 index 000000000..fd4821fc6 --- /dev/null +++ b/tests/Fixture/ResetDatabase/migration-configs/migration-configuration-transactional.php @@ -0,0 +1,17 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +return [ + 'migrations_paths' => [ + 'Zenstruck\\Foundry\\Tests\\Fixture\\ResetDatabase\\Migrations' => \dirname(__DIR__, 4).'/var/cache/Migrations', + ], + 'transactional' => true, +]; diff --git a/tests/Fixture/ResetDatabase/migration-configs/migration-configuration.php b/tests/Fixture/ResetDatabase/migration-configs/migration-configuration.php new file mode 100644 index 000000000..c47f48ae0 --- /dev/null +++ b/tests/Fixture/ResetDatabase/migration-configs/migration-configuration.php @@ -0,0 +1,17 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +return [ + 'migrations_paths' => [ + 'Zenstruck\\Foundry\\Tests\\Fixture\\ResetDatabase\\Migrations' => \dirname(__DIR__, 4).'/var/cache/Migrations', + ], + 'transactional' => false, +]; diff --git a/tests/Fixture/SomeEnum.php b/tests/Fixture/SomeEnum.php index f91d224c6..a874a6fbc 100644 --- a/tests/Fixture/SomeEnum.php +++ b/tests/Fixture/SomeEnum.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Zenstruck\Foundry\Tests\Fixture; enum SomeEnum diff --git a/tests/Fixture/Stories/ServiceStory.php b/tests/Fixture/Stories/ServiceStory.php new file mode 100644 index 000000000..d4b4edf4a --- /dev/null +++ b/tests/Fixture/Stories/ServiceStory.php @@ -0,0 +1,35 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Tests\Fixture\Stories; + +use Symfony\Component\Routing\RouterInterface; +use Zenstruck\Foundry\Story; +use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\GenericEntityFactory; + +/** + * @author Nicolas PHILIPPE + */ +final class ServiceStory extends Story +{ + public function __construct( + private readonly RouterInterface $router, + ) { + } + + public function build(): void + { + $this->addState( + 'foo', + GenericEntityFactory::createOne(['prop1' => $this->router->getContext()->getHost()]) + ); + } +} diff --git a/tests/Fixture/TestKernel.php b/tests/Fixture/TestKernel.php index 63e618506..c99d69c31 100644 --- a/tests/Fixture/TestKernel.php +++ b/tests/Fixture/TestKernel.php @@ -11,160 +11,51 @@ namespace Zenstruck\Foundry\Tests\Fixture; -use DAMA\DoctrineTestBundle\DAMADoctrineTestBundle; -use Doctrine\Bundle\DoctrineBundle\DoctrineBundle; -use Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle; -use Doctrine\Bundle\MongoDBBundle\DoctrineMongoDBBundle; -use Psr\Log\NullLogger; -use Symfony\Bundle\FrameworkBundle\FrameworkBundle; -use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait; use Symfony\Bundle\MakerBundle\MakerBundle; use Symfony\Component\Config\Loader\LoaderInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; -use Symfony\Component\HttpKernel\Kernel; -use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; use Zenstruck\Foundry\ORM\ResetDatabase\ResetDatabaseMode; +use Zenstruck\Foundry\Tests\Fixture\Events\FoundryEventListener; use Zenstruck\Foundry\Tests\Fixture\Factories\ArrayFactory; use Zenstruck\Foundry\Tests\Fixture\Factories\Object1Factory; -use Zenstruck\Foundry\Tests\Fixture\Stories\GlobalInvokableService; -use Zenstruck\Foundry\Tests\Fixture\Stories\GlobalStory; -use Zenstruck\Foundry\ZenstruckFoundryBundle; +use Zenstruck\Foundry\Tests\Fixture\InMemory\InMemoryAddressRepository; +use Zenstruck\Foundry\Tests\Fixture\InMemory\InMemoryContactRepository; +use Zenstruck\Foundry\Tests\Fixture\Stories\ServiceStory; /** * @author Kevin Bond */ -final class TestKernel extends Kernel +final class TestKernel extends FoundryTestKernel { - use MicroKernelTrait; - public function registerBundles(): iterable { - yield new FrameworkBundle(); - yield new MakerBundle(); - - if (\getenv('DATABASE_URL')) { - yield new DoctrineBundle(); - yield new DoctrineMigrationsBundle(); - } - - if (\getenv('MONGO_URL')) { - yield new DoctrineMongoDBBundle(); - } - - yield new ZenstruckFoundryBundle(); + yield from parent::registerBundles(); - if (\getenv('USE_DAMA_DOCTRINE_TEST_BUNDLE')) { - yield new DAMADoctrineTestBundle(); - } + yield new MakerBundle(); } protected function configureContainer(ContainerBuilder $c, LoaderInterface $loader): void { - $c->loadFromExtension('framework', [ - 'http_method_override' => false, - 'secret' => 'S3CRET', - 'router' => ['utf8' => true], - 'test' => true, - ]); + parent::configureContainer($c, $loader); + + if ('dev' !== $this->getEnvironment()) { + $loader->load(\sprintf('%s/config/%s.yaml', __DIR__, $this->getEnvironment())); + } $c->loadFromExtension('zenstruck_foundry', [ - 'global_state' => [ - GlobalStory::class, - GlobalInvokableService::class, - ], 'orm' => [ 'reset' => [ - 'mode' => \getenv('DATABASE_RESET_MODE') ?: ResetDatabaseMode::SCHEMA, + 'mode' => ResetDatabaseMode::SCHEMA, ], ], ]); - if (\getenv('DATABASE_URL')) { - $c->loadFromExtension('doctrine', [ - 'dbal' => ['url' => '%env(resolve:DATABASE_URL)%', 'use_savepoints' => true], - 'orm' => [ - 'auto_generate_proxy_classes' => true, - 'auto_mapping' => true, - 'mappings' => [ - 'Entity' => [ - 'is_bundle' => false, - 'type' => 'attribute', - 'dir' => '%kernel.project_dir%/tests/Fixture/Entity', - 'prefix' => 'Zenstruck\Foundry\Tests\Fixture\Entity', - 'alias' => 'Entity', - ], - 'Model' => [ - 'is_bundle' => false, - 'type' => 'attribute', - 'dir' => '%kernel.project_dir%/tests/Fixture/Model', - 'prefix' => 'Zenstruck\Foundry\Tests\Fixture\Model', - 'alias' => 'Model', - ], - ], - 'controller_resolver' => ['auto_mapping' => true], - ], - ]); - - if (ResetDatabaseMode::MIGRATE->value === \getenv('DATABASE_RESET_MODE')) { - $c->loadFromExtension('doctrine', [ - 'orm' => [ - 'mappings' => [ - 'Migrate' => [ - 'is_bundle' => false, - 'type' => 'attribute', - 'dir' => '%kernel.project_dir%/tests/Fixture/EdgeCases/Migrate/ORM', - 'prefix' => 'Zenstruck\Foundry\Tests\Fixture\EdgeCases\Migrate\ORM', - 'alias' => 'Migrate', - ], - ], - ], - ]); - } - - $c->loadFromExtension('doctrine_migrations', [ - 'migrations_paths' => [ - 'Zenstruck\\Foundry\\Tests\\Fixture\\Migrations' => '%kernel.project_dir%/tests/Fixture/Migrations', - ], - ]); - } - - if (\getenv('MONGO_URL')) { - $c->loadFromExtension('doctrine_mongodb', [ - 'connections' => [ - 'default' => ['server' => '%env(resolve:MONGO_URL)%'], - ], - 'default_database' => 'mongo', - 'document_managers' => [ - 'default' => [ - 'auto_mapping' => true, - 'mappings' => [ - 'Document' => [ - 'is_bundle' => false, - 'type' => 'attribute', - 'dir' => '%kernel.project_dir%/tests/Fixture/Document', - 'prefix' => 'Zenstruck\Foundry\Tests\Fixture\Document', - 'alias' => 'Document', - ], - 'Model' => [ - 'is_bundle' => false, - 'type' => 'attribute', - 'dir' => '%kernel.project_dir%/tests/Fixture/Model', - 'prefix' => 'Zenstruck\Foundry\Tests\Fixture\Model', - 'alias' => 'Model', - ], - ], - ], - ], - ]); - } - - $c->register('logger', NullLogger::class); - $c->register(GlobalInvokableService::class); $c->register(ArrayFactory::class)->setAutowired(true)->setAutoconfigured(true); $c->register(Object1Factory::class)->setAutowired(true)->setAutoconfigured(true); - } + $c->register(ServiceStory::class)->setAutowired(true)->setAutoconfigured(true); + $c->register(InMemoryAddressRepository::class)->setAutowired(true)->setAutoconfigured(true); + $c->register(InMemoryContactRepository::class)->setAutowired(true)->setAutoconfigured(true); - protected function configureRoutes(RoutingConfigurator $routes): void - { + $c->register(FoundryEventListener::class)->setAutowired(true)->setAutoconfigured(true); } } diff --git a/tests/Fixture/config/always_force.yaml b/tests/Fixture/config/always_force.yaml new file mode 100644 index 000000000..ab286dba7 --- /dev/null +++ b/tests/Fixture/config/always_force.yaml @@ -0,0 +1,3 @@ +zenstruck_foundry: + instantiator: + always_force_properties: true # always "force set" properties diff --git a/tests/Integration/Attribute/WithStory/ParentClassWithStoryAttributeTestCase.php b/tests/Integration/Attribute/WithStory/ParentClassWithStoryAttributeTestCase.php new file mode 100644 index 000000000..0a3369277 --- /dev/null +++ b/tests/Integration/Attribute/WithStory/ParentClassWithStoryAttributeTestCase.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Tests\Integration\Attribute\WithStory; + +use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; +use Zenstruck\Foundry\Attribute\WithStory; +use Zenstruck\Foundry\Test\Factories; +use Zenstruck\Foundry\Test\ResetDatabase; +use Zenstruck\Foundry\Tests\Fixture\Stories\EntityStory; +use Zenstruck\Foundry\Tests\Integration\RequiresORM; + +/** + * @author Nicolas PHILIPPE + */ +#[WithStory(EntityStory::class)] +abstract class ParentClassWithStoryAttributeTestCase extends KernelTestCase +{ + use Factories, RequiresORM, ResetDatabase; +} diff --git a/tests/Integration/Attribute/WithStory/WithStoryOnClassTest.php b/tests/Integration/Attribute/WithStory/WithStoryOnClassTest.php new file mode 100644 index 000000000..b64777c46 --- /dev/null +++ b/tests/Integration/Attribute/WithStory/WithStoryOnClassTest.php @@ -0,0 +1,71 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Tests\Integration\Attribute\WithStory; + +use PHPUnit\Framework\Attributes\RequiresPhpunit; +use PHPUnit\Framework\Attributes\RequiresPhpunitExtension; +use PHPUnit\Framework\Attributes\Test; +use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; +use Zenstruck\Foundry\Attribute\WithStory; +use Zenstruck\Foundry\PHPUnit\FoundryExtension; +use Zenstruck\Foundry\Test\Factories; +use Zenstruck\Foundry\Test\ResetDatabase; +use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\GenericEntityFactory; +use Zenstruck\Foundry\Tests\Fixture\Stories\EntityPoolStory; +use Zenstruck\Foundry\Tests\Fixture\Stories\EntityStory; +use Zenstruck\Foundry\Tests\Integration\RequiresORM; + +/** + * @author Nicolas PHILIPPE + * @requires PHPUnit >=11.0 + */ +#[RequiresPhpunit('>=11.0')] +#[RequiresPhpunitExtension(FoundryExtension::class)] +#[WithStory(EntityStory::class)] +final class WithStoryOnClassTest extends KernelTestCase +{ + use Factories, RequiresORM, ResetDatabase; + + /** + * @test + */ + #[Test] + public function can_use_story_in_attribute(): void + { + GenericEntityFactory::assert()->count(2); + + // ensure state is accessible + $this->assertSame('foo', EntityStory::get('foo')->getProp1()); + } + + /** + * @test + */ + #[Test] + #[WithStory(EntityStory::class)] + public function can_use_story_in_attribute_multiple_times(): void + { + GenericEntityFactory::assert()->count(2); + } + + /** + * @test + */ + #[Test] + #[WithStory(EntityPoolStory::class)] + public function can_use_another_story_at_level_class(): void + { + GenericEntityFactory::assert()->count(5); + } +} diff --git a/tests/Integration/Attribute/WithStory/WithStoryOnMethodTest.php b/tests/Integration/Attribute/WithStory/WithStoryOnMethodTest.php new file mode 100644 index 000000000..c6f0df929 --- /dev/null +++ b/tests/Integration/Attribute/WithStory/WithStoryOnMethodTest.php @@ -0,0 +1,73 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Tests\Integration\Attribute\WithStory; + +use PHPUnit\Framework\Attributes\RequiresPhpunit; +use PHPUnit\Framework\Attributes\RequiresPhpunitExtension; +use PHPUnit\Framework\Attributes\Test; +use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; +use Zenstruck\Foundry\Attribute\WithStory; +use Zenstruck\Foundry\PHPUnit\FoundryExtension; +use Zenstruck\Foundry\Test\Factories; +use Zenstruck\Foundry\Test\ResetDatabase; +use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\GenericEntityFactory; +use Zenstruck\Foundry\Tests\Fixture\Stories\EntityPoolStory; +use Zenstruck\Foundry\Tests\Fixture\Stories\EntityStory; +use Zenstruck\Foundry\Tests\Fixture\Stories\ServiceStory; +use Zenstruck\Foundry\Tests\Integration\RequiresORM; + +/** + * @author Nicolas PHILIPPE + * @requires PHPUnit >=11.0 + */ +#[RequiresPhpunit('>=11.0')] +#[RequiresPhpunitExtension(FoundryExtension::class)] +final class WithStoryOnMethodTest extends KernelTestCase +{ + use Factories, RequiresORM, ResetDatabase; + + /** + * @test + */ + #[Test] + #[WithStory(EntityStory::class)] + public function can_use_story_in_attribute(): void + { + GenericEntityFactory::assert()->count(2); + + // ensure state is accessible + $this->assertSame('foo', EntityStory::get('foo')->getProp1()); + } + + /** + * @test + */ + #[Test] + #[WithStory(EntityStory::class)] + #[WithStory(EntityPoolStory::class)] + public function can_use_multiple_story_in_attribute(): void + { + GenericEntityFactory::assert()->count(5); + } + + /** + * @test + */ + #[Test] + #[WithStory(ServiceStory::class)] + public function can_use_service_story(): void + { + $this->assertSame('localhost', ServiceStory::get('foo')->getProp1()); + } +} diff --git a/tests/Integration/Attribute/WithStory/WithStoryOnParentClassTest.php b/tests/Integration/Attribute/WithStory/WithStoryOnParentClassTest.php new file mode 100644 index 000000000..84c199576 --- /dev/null +++ b/tests/Integration/Attribute/WithStory/WithStoryOnParentClassTest.php @@ -0,0 +1,41 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Tests\Integration\Attribute\WithStory; + +use PHPUnit\Framework\Attributes\RequiresPhpunit; +use PHPUnit\Framework\Attributes\RequiresPhpunitExtension; +use PHPUnit\Framework\Attributes\Test; +use Zenstruck\Foundry\Attribute\WithStory; +use Zenstruck\Foundry\PHPUnit\FoundryExtension; +use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\GenericEntityFactory; +use Zenstruck\Foundry\Tests\Fixture\Stories\EntityPoolStory; + +/** + * @author Nicolas PHILIPPE + * @requires PHPUnit >=11.0 + */ +#[RequiresPhpunit('>=11.0')] +#[RequiresPhpunitExtension(FoundryExtension::class)] +#[WithStory(EntityPoolStory::class)] +final class WithStoryOnParentClassTest extends ParentClassWithStoryAttributeTestCase +{ + /** + * @test + */ + #[Test] + public function can_use_story_in_attribute_from_parent_class(): void + { + GenericEntityFactory::assert()->count(5); + } +} diff --git a/tests/Integration/DataProvider/DataProviderForServiceFactoryInKernelTestCaseTest.php b/tests/Integration/DataProvider/DataProviderForServiceFactoryInKernelTestCaseTest.php new file mode 100644 index 000000000..579e34db3 --- /dev/null +++ b/tests/Integration/DataProvider/DataProviderForServiceFactoryInKernelTestCaseTest.php @@ -0,0 +1,56 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Tests\Integration\DataProvider; + +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\RequiresPhpunit; +use PHPUnit\Framework\Attributes\RequiresPhpunitExtension; +use PHPUnit\Framework\Attributes\Test; +use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; +use Zenstruck\Foundry\Configuration; +use Zenstruck\Foundry\PHPUnit\FoundryExtension; +use Zenstruck\Foundry\Test\Factories; +use Zenstruck\Foundry\Tests\Fixture\Factories\Object1Factory; +use Zenstruck\Foundry\Tests\Fixture\Object1; + +use function Zenstruck\Foundry\faker; + +/** + * @author Nicolas PHILIPPE + * @requires PHPUnit >=11.4 + */ +#[RequiresPhpunit('>=11.4')] +#[RequiresPhpunitExtension(FoundryExtension::class)] +final class DataProviderForServiceFactoryInKernelTestCaseTest extends KernelTestCase +{ + use Factories; + + #[Test] + #[DataProvider('createObjectFromServiceFactoryInDataProvider')] + public function it_can_create_one_object_in_data_provider(?Object1 $providedData, string $expected): void + { + self::assertFalse(Configuration::instance()->inADataProvider()); + + self::assertInstanceOf(Object1::class, $providedData); + $this->assertSame($expected, $providedData->getProp1()); + } + + public static function createObjectFromServiceFactoryInDataProvider(): iterable + { + yield 'service factory' => [ + Object1Factory::createOne(['prop1' => $prop1 = faker()->sentence()]), + "{$prop1}-constructor", + ]; + } +} diff --git a/tests/Integration/DataProvider/DataProviderInUnitTest.php b/tests/Integration/DataProvider/DataProviderInUnitTest.php new file mode 100644 index 000000000..e822754c3 --- /dev/null +++ b/tests/Integration/DataProvider/DataProviderInUnitTest.php @@ -0,0 +1,82 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Tests\Integration\DataProvider; + +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\RequiresPhpunit; +use PHPUnit\Framework\Attributes\RequiresPhpunitExtension; +use PHPUnit\Framework\Attributes\Test; +use PHPUnit\Framework\TestCase; +use Zenstruck\Foundry\PHPUnit\FoundryExtension; +use Zenstruck\Foundry\Test\Factories; +use Zenstruck\Foundry\Tests\Fixture\Entity\GenericEntity; +use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\GenericEntityFactory; +use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\GenericProxyEntityFactory; +use Zenstruck\Foundry\Tests\Fixture\Factories\Object1Factory; +use Zenstruck\Foundry\Tests\Fixture\Factories\Object2Factory; +use Zenstruck\Foundry\Tests\Fixture\Object1; +use Zenstruck\Foundry\Tests\Fixture\Object2; + +use function Zenstruck\Foundry\faker; +use function Zenstruck\Foundry\Persistence\unproxy; + +/** + * @author Nicolas PHILIPPE + * @requires PHPUnit >=11.4 + */ +#[RequiresPhpunit('>=11.4')] +#[RequiresPhpunitExtension(FoundryExtension::class)] +final class DataProviderInUnitTest extends TestCase +{ + use Factories; + + #[Test] + #[DataProvider('createObjectWithObjectFactoryInDataProvider')] + public function assert_it_can_create_object_with_object_factory_in_data_provider(mixed $providedData, mixed $expectedData): void + { + self::assertEquals($expectedData, $providedData); + } + + public static function createObjectWithObjectFactoryInDataProvider(): iterable + { + yield 'object factory' => [Object2Factory::createOne(['object' => new Object1('prop1')]), new Object2(new Object1('prop1'))]; + yield 'service factory can be used if dependency is optional' => [Object1Factory::createOne(), new Object1('value1')]; + } + + #[Test] + #[DataProvider('createObjectWithPersistentObjectFactoryInDataProvider')] + public function assert_it_can_create_object_with_persistent_factory_in_data_provider(mixed $providedData, mixed $expectedData): void + { + self::assertEquals($expectedData, unproxy($providedData)); + } + + public static function createObjectWithPersistentObjectFactoryInDataProvider(): iterable + { + yield 'persistent factory' => [GenericEntityFactory::createOne(), new GenericEntity('default1')]; + yield 'proxy persistent factory' => [GenericProxyEntityFactory::createOne(), new GenericEntity('default1')]; + } + + #[Test] + #[DataProvider('createObjectUsingFakerInDataProvider')] + public function assert_it_can_create_use_faker_in_data_provider(mixed $providedData, string $expected): void + { + self::assertSame($expected, $providedData->getProp1()); + } + + public static function createObjectUsingFakerInDataProvider(): iterable + { + yield 'object factory' => [Object1Factory::createOne(['prop1' => $prop1 = faker()->sentence()]), "{$prop1}-constructor"]; + yield 'persistent factory' => [GenericEntityFactory::createOne(['prop1' => $prop1 = faker()->sentence()]), $prop1]; + } +} diff --git a/tests/Integration/DataProvider/DataProviderWithInMemoryTest.php b/tests/Integration/DataProvider/DataProviderWithInMemoryTest.php new file mode 100644 index 000000000..03e7149ba --- /dev/null +++ b/tests/Integration/DataProvider/DataProviderWithInMemoryTest.php @@ -0,0 +1,99 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Tests\Integration\DataProvider; + +use Doctrine\ORM\EntityManagerInterface; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\RequiresPhpunit; +use PHPUnit\Framework\Attributes\RequiresPhpunitExtension; +use PHPUnit\Framework\Attributes\Test; +use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; +use Zenstruck\Foundry\InMemory\AsInMemoryTest; +use Zenstruck\Foundry\Persistence\PersistentObjectFactory; +use Zenstruck\Foundry\PHPUnit\FoundryExtension; +use Zenstruck\Foundry\Test\Factories; +use Zenstruck\Foundry\Test\ResetDatabase; +use Zenstruck\Foundry\Tests\Fixture\Entity\Contact; +use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\Contact\ContactFactory; +use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\Contact\ProxyContactFactory; +use Zenstruck\Foundry\Tests\Fixture\InMemory\InMemoryContactRepository; +use Zenstruck\Foundry\Tests\Integration\RequiresORM; + +use function Zenstruck\Foundry\Persistence\unproxy; + +/** + * @author Nicolas PHILIPPE + * @requires PHPUnit 11.4 + */ +#[RequiresPhpunit('11.4')] +#[RequiresPhpunitExtension(FoundryExtension::class)] +final class DataProviderWithInMemoryTest extends KernelTestCase +{ + use Factories; + use RequiresORM; // needed to use the entity manager + use ResetDatabase; + + private InMemoryContactRepository $contactRepository; + + private EntityManagerInterface $entityManager; + + protected function setUp(): void + { + $this->contactRepository = self::getContainer()->get(InMemoryContactRepository::class); // @phpstan-ignore assign.propertyType + + $this->entityManager = self::getContainer()->get(EntityManagerInterface::class); // @phpstan-ignore assign.propertyType + } + + /** + * @param PersistentObjectFactory $factory + */ + #[Test] + #[DataProvider('provideContactFactory')] + #[AsInMemoryTest] + public function it_can_create_in_memory_factory_in_data_provider(PersistentObjectFactory $factory): void + { + if ('1' !== ($_ENV['USE_FOUNDRY_PHPUNIT_EXTENSION'] ?? null)) { + self::markTestSkipped('Needs Foundry PHPUnit extension.'); + } + + $contact = $factory->create(); + + self::assertSame([unproxy($contact)], $this->contactRepository->_all()); + + self::assertSame(0, $this->entityManager->getRepository(Contact::class)->count()); + } + + public static function provideContactFactory(): iterable + { + yield [ContactFactory::new()]; + yield [ProxyContactFactory::new()]; + } + + #[Test] + #[DataProvider('provideContact')] + #[AsInMemoryTest] + public function it_can_create_in_memory_objects_in_data_provider(?Contact $contact = null): void + { + self::assertInstanceOf(Contact::class, $contact); + + self::assertSame([unproxy($contact)], $this->contactRepository->_all()); + + self::assertSame(0, $this->entityManager->getRepository(Contact::class)->count()); + } + + public static function provideContact(): iterable + { + yield [ProxyContactFactory::createOne()]; + } +} diff --git a/tests/Integration/DataProvider/DataProviderWithNonProxyFactoryInKernelTestCaseTest.php b/tests/Integration/DataProvider/DataProviderWithNonProxyFactoryInKernelTestCaseTest.php new file mode 100644 index 000000000..b948ec08e --- /dev/null +++ b/tests/Integration/DataProvider/DataProviderWithNonProxyFactoryInKernelTestCaseTest.php @@ -0,0 +1,54 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Tests\Integration\DataProvider; + +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\RequiresPhpunit; +use PHPUnit\Framework\Attributes\RequiresPhpunitExtension; +use PHPUnit\Framework\Attributes\Test; +use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; +use Zenstruck\Foundry\PHPUnit\FoundryExtension; +use Zenstruck\Foundry\Test\Factories; +use Zenstruck\Foundry\Test\ResetDatabase; +use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\GenericEntityFactory; + +/** + * @author Nicolas PHILIPPE + * @requires PHPUnit >=11.4 + */ +#[RequiresPhpunit('>=11.4')] +#[RequiresPhpunitExtension(FoundryExtension::class)] +final class DataProviderWithNonProxyFactoryInKernelTestCaseTest extends KernelTestCase +{ + use Factories; + use ResetDatabase; + + #[Test] + #[DataProvider('throwsExceptionWhenCreatingObjectInDataProvider')] + public function it_throws_an_exception_when_trying_to_create_an_object_in_data_provider(?\Throwable $e): void + { + self::assertInstanceOf(\LogicException::class, $e); + self::assertStringStartsWith('Cannot create object in a data provider for non-proxy factories.', $e->getMessage()); + } + + public static function throwsExceptionWhenCreatingObjectInDataProvider(): iterable + { + try { + GenericEntityFactory::createOne(); + } catch (\Throwable $e) { + } + + yield [$e ?? null]; + } +} diff --git a/tests/Integration/DataProvider/DataProviderWithProxyFactoryInKernelTestCase.php b/tests/Integration/DataProvider/DataProviderWithProxyFactoryInKernelTestCase.php new file mode 100644 index 000000000..c48ebc4ff --- /dev/null +++ b/tests/Integration/DataProvider/DataProviderWithProxyFactoryInKernelTestCase.php @@ -0,0 +1,94 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Tests\Integration\DataProvider; + +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\RequiresPhpunit; +use PHPUnit\Framework\Attributes\RequiresPhpunitExtension; +use PHPUnit\Framework\Attributes\Test; +use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; +use Zenstruck\Foundry\Persistence\PersistentProxyObjectFactory; +use Zenstruck\Foundry\Persistence\Proxy; +use Zenstruck\Foundry\PHPUnit\FoundryExtension; +use Zenstruck\Foundry\Test\Factories; +use Zenstruck\Foundry\Test\ResetDatabase; +use Zenstruck\Foundry\Tests\Fixture\Model\GenericModel; + +use function Zenstruck\Foundry\Persistence\unproxy; + +/** + * @author Nicolas PHILIPPE + * @requires PHPUnit >=11.4 + */ +#[RequiresPhpunit('>=11.4')] +#[RequiresPhpunitExtension(FoundryExtension::class)] +abstract class DataProviderWithProxyFactoryInKernelTestCase extends KernelTestCase +{ + use Factories; + use ResetDatabase; + + #[Test] + #[DataProvider('createOneObjectInDataProvider')] + public function assert_it_can_create_one_object_in_data_provider(?GenericModel $providedData): void + { + static::factory()::assert()->count(1); + + self::assertInstanceOf(Proxy::class, $providedData); + self::assertNotInstanceOf(Proxy::class, unproxy($providedData)); // asserts two proxies are not nested + self::assertSame('value set in data provider', $providedData->getProp1()); + } + + public static function createOneObjectInDataProvider(): iterable + { + yield 'createOne()' => [ + static::factory()::createOne(['prop1' => 'value set in data provider']), + ]; + + yield 'create()' => [ + static::factory()->create(['prop1' => 'value set in data provider']), + ]; + } + + #[Test] + #[DataProvider('createMultipleObjectsInDataProvider')] + public function assert_it_can_create_multiple_objects_in_data_provider(?array $providedData): void + { + self::assertIsArray($providedData); + static::factory()::assert()->count(2); + self::assertSame('prop 1', $providedData[0]->getProp1()); + self::assertSame('prop 2', $providedData[1]->getProp1()); + } + + public static function createMultipleObjectsInDataProvider(): iterable + { + yield 'createSequence()' => [ + static::factory()::createSequence([ + ['prop1' => 'prop 1'], + ['prop1' => 'prop 2'], + ]), + ]; + + yield 'FactoryCollection::create()' => [ + static::factory()->sequence([ + ['prop1' => 'prop 1'], + ['prop1' => 'prop 2'], + ])->create(), + ]; + } + + /** + * @return PersistentProxyObjectFactory + */ + abstract protected static function factory(): PersistentProxyObjectFactory; +} diff --git a/tests/Integration/DataProvider/GenericDocumentProxyFactoryTest.php b/tests/Integration/DataProvider/GenericDocumentProxyFactoryTest.php new file mode 100644 index 000000000..058892c29 --- /dev/null +++ b/tests/Integration/DataProvider/GenericDocumentProxyFactoryTest.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\Integration\DataProvider; + +use PHPUnit\Framework\Attributes\RequiresPhpunit; +use PHPUnit\Framework\Attributes\RequiresPhpunitExtension; +use Zenstruck\Foundry\PHPUnit\FoundryExtension; +use Zenstruck\Foundry\Tests\Fixture\Factories\Document\GenericProxyDocumentFactory; +use Zenstruck\Foundry\Tests\Integration\RequiresMongo; + +/** + * @author Nicolas PHILIPPE + * @requires PHPUnit >=11.4 + */ +#[RequiresPhpunit('>=11.4')] +#[RequiresPhpunitExtension(FoundryExtension::class)] +final class GenericDocumentProxyFactoryTest extends DataProviderWithProxyFactoryInKernelTestCase +{ + use RequiresMongo; + + protected static function factory(): GenericProxyDocumentFactory + { + return GenericProxyDocumentFactory::new(); + } +} diff --git a/tests/Integration/DataProvider/GenericEntityProxyFactoryTest.php b/tests/Integration/DataProvider/GenericEntityProxyFactoryTest.php new file mode 100644 index 000000000..1658df558 --- /dev/null +++ b/tests/Integration/DataProvider/GenericEntityProxyFactoryTest.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\Integration\DataProvider; + +use PHPUnit\Framework\Attributes\RequiresPhpunit; +use PHPUnit\Framework\Attributes\RequiresPhpunitExtension; +use Zenstruck\Foundry\PHPUnit\FoundryExtension; +use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\GenericProxyEntityFactory; +use Zenstruck\Foundry\Tests\Integration\RequiresORM; + +/** + * @author Nicolas PHILIPPE + * @requires PHPUnit >=11.4 + */ +#[RequiresPhpunit('>=11.4')] +#[RequiresPhpunitExtension(FoundryExtension::class)] +final class GenericEntityProxyFactoryTest extends DataProviderWithProxyFactoryInKernelTestCase +{ + use RequiresORM; + + protected static function factory(): GenericProxyEntityFactory + { + return GenericProxyEntityFactory::new(); + } +} diff --git a/tests/Integration/EventsTest.php b/tests/Integration/EventsTest.php new file mode 100644 index 000000000..25398d187 --- /dev/null +++ b/tests/Integration/EventsTest.php @@ -0,0 +1,48 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Tests\Integration; + +use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; +use Zenstruck\Foundry\Test\Factories; +use Zenstruck\Foundry\Test\ResetDatabase; +use Zenstruck\Foundry\Tests\Fixture\Events\FactoryWithEventListeners; + +final class EventsTest extends KernelTestCase +{ + use Factories, RequiresORM, ResetDatabase; + + /** + * @test + */ + public function it_can_call_hooks(): void + { + $address = FactoryWithEventListeners::createOne(['name' => 'events']); + + self::assertSame( + <<name + ); + } +} diff --git a/tests/Integration/ForceFactoriesTraitUsage/KernelTestCaseWithBothTraitsInWrongOrderTest.php b/tests/Integration/ForceFactoriesTraitUsage/KernelTestCaseWithBothTraitsInWrongOrderTest.php new file mode 100644 index 000000000..23e72634a --- /dev/null +++ b/tests/Integration/ForceFactoriesTraitUsage/KernelTestCaseWithBothTraitsInWrongOrderTest.php @@ -0,0 +1,45 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Tests\Integration\ForceFactoriesTraitUsage; + +use PHPUnit\Framework\Attributes\RequiresPhpunit; +use PHPUnit\Framework\Attributes\Test; +use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; +use Zenstruck\Foundry\Test\Factories; +use Zenstruck\Foundry\Test\ResetDatabase; +use Zenstruck\Foundry\Tests\Fixture\Factories\Object1Factory; + +#[RequiresPhpunit('>=11.0')] +final class KernelTestCaseWithBothTraitsInWrongOrderTest extends KernelTestCase +{ + use Factories, ResetDatabase; + + #[Test] + public function should_not_throw(): void + { + Object1Factory::createOne(); + + $this->expectNotToPerformAssertions(); + } + + #[Test] + public function should_not_throw_even_when_kernel_is_booted(): void + { + self::getContainer()->get('.zenstruck_foundry.configuration'); + + Object1Factory::createOne(); + + $this->expectNotToPerformAssertions(); + } +} diff --git a/tests/Integration/ForceFactoriesTraitUsage/KernelTestCaseWithBothTraitsTest.php b/tests/Integration/ForceFactoriesTraitUsage/KernelTestCaseWithBothTraitsTest.php new file mode 100644 index 000000000..fd6de3f93 --- /dev/null +++ b/tests/Integration/ForceFactoriesTraitUsage/KernelTestCaseWithBothTraitsTest.php @@ -0,0 +1,45 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Tests\Integration\ForceFactoriesTraitUsage; + +use PHPUnit\Framework\Attributes\RequiresPhpunit; +use PHPUnit\Framework\Attributes\Test; +use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; +use Zenstruck\Foundry\Test\Factories; +use Zenstruck\Foundry\Test\ResetDatabase; +use Zenstruck\Foundry\Tests\Fixture\Factories\Object1Factory; + +#[RequiresPhpunit('>=11.0')] +final class KernelTestCaseWithBothTraitsTest extends KernelTestCase +{ + use Factories, ResetDatabase; + + #[Test] + public function should_not_throw(): void + { + Object1Factory::createOne(); + + $this->expectNotToPerformAssertions(); + } + + #[Test] + public function should_not_throw_even_when_kernel_is_booted(): void + { + self::getContainer()->get('.zenstruck_foundry.configuration'); + + Object1Factory::createOne(); + + $this->expectNotToPerformAssertions(); + } +} diff --git a/tests/Integration/ForceFactoriesTraitUsage/KernelTestCaseWithFactoriesTraitTest.php b/tests/Integration/ForceFactoriesTraitUsage/KernelTestCaseWithFactoriesTraitTest.php new file mode 100644 index 000000000..80d40751d --- /dev/null +++ b/tests/Integration/ForceFactoriesTraitUsage/KernelTestCaseWithFactoriesTraitTest.php @@ -0,0 +1,44 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Tests\Integration\ForceFactoriesTraitUsage; + +use PHPUnit\Framework\Attributes\RequiresPhpunit; +use PHPUnit\Framework\Attributes\Test; +use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; +use Zenstruck\Foundry\Test\Factories; +use Zenstruck\Foundry\Tests\Fixture\Factories\Object1Factory; + +#[RequiresPhpunit('>=11.0')] +final class KernelTestCaseWithFactoriesTraitTest extends KernelTestCase +{ + use Factories; + + #[Test] + public function should_not_throw(): void + { + Object1Factory::createOne(); + + $this->expectNotToPerformAssertions(); + } + + #[Test] + public function should_not_throw_even_when_kernel_is_booted(): void + { + self::getContainer()->get('.zenstruck_foundry.configuration'); + + Object1Factory::createOne(); + + $this->expectNotToPerformAssertions(); + } +} diff --git a/tests/Integration/ForceFactoriesTraitUsage/KernelTestCaseWithOnlyResetDatabaseTraitTest.php b/tests/Integration/ForceFactoriesTraitUsage/KernelTestCaseWithOnlyResetDatabaseTraitTest.php new file mode 100644 index 000000000..6727c074f --- /dev/null +++ b/tests/Integration/ForceFactoriesTraitUsage/KernelTestCaseWithOnlyResetDatabaseTraitTest.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\Tests\Integration\ForceFactoriesTraitUsage; + +use PHPUnit\Framework\Attributes\RequiresPhpunit; +use Zenstruck\Foundry\Test\ResetDatabase; + +#[RequiresPhpunit('>=11.0')] +final class KernelTestCaseWithOnlyResetDatabaseTraitTest extends KernelTestCaseWithoutFactoriesTraitTestCase +{ + use ResetDatabase; +} diff --git a/tests/Integration/ForceFactoriesTraitUsage/KernelTestCaseWithoutFactoriesTraitTest.php b/tests/Integration/ForceFactoriesTraitUsage/KernelTestCaseWithoutFactoriesTraitTest.php new file mode 100644 index 000000000..dae664edc --- /dev/null +++ b/tests/Integration/ForceFactoriesTraitUsage/KernelTestCaseWithoutFactoriesTraitTest.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Tests\Integration\ForceFactoriesTraitUsage; + +use PHPUnit\Framework\Attributes\RequiresPhpunit; + +#[RequiresPhpunit('>=11.0')] +final class KernelTestCaseWithoutFactoriesTraitTest extends KernelTestCaseWithoutFactoriesTraitTestCase +{ +} diff --git a/tests/Integration/ForceFactoriesTraitUsage/KernelTestCaseWithoutFactoriesTraitTestCase.php b/tests/Integration/ForceFactoriesTraitUsage/KernelTestCaseWithoutFactoriesTraitTestCase.php new file mode 100644 index 000000000..a332ade2e --- /dev/null +++ b/tests/Integration/ForceFactoriesTraitUsage/KernelTestCaseWithoutFactoriesTraitTestCase.php @@ -0,0 +1,96 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Tests\Integration\ForceFactoriesTraitUsage; + +use PHPUnit\Framework\Attributes\After; +use PHPUnit\Framework\Attributes\Before; +use PHPUnit\Framework\Attributes\IgnoreDeprecations; +use PHPUnit\Framework\Attributes\RequiresPhpunitExtension; +use PHPUnit\Framework\Attributes\Test; +use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; +use Zenstruck\Foundry\Configuration; +use Zenstruck\Foundry\PHPUnit\FoundryExtension; +use Zenstruck\Foundry\Tests\Fixture\Factories\Object1Factory; +use Zenstruck\Foundry\Tests\Fixture\Stories\ObjectStory; + +abstract class KernelTestCaseWithoutFactoriesTraitTestCase extends KernelTestCase +{ + #[Test] + public function not_using_foundry_should_not_throw(): void + { + $this->expectNotToPerformAssertions(); + } + + #[Test] + public function not_using_foundry_should_not_throw_even_when_container_is_used(): void + { + self::getContainer()->get('.zenstruck_foundry.configuration'); + + $this->expectNotToPerformAssertions(); + } + + #[Test] + #[IgnoreDeprecations] + public function using_foundry_without_trait_should_throw(): void + { + $this->assertDeprecation(); + + Object1Factory::createOne(); + } + + #[Test] + #[IgnoreDeprecations] + public function using_foundry_without_trait_should_throw_even_when_kernel_is_booted(): void + { + $this->assertDeprecation(); + + self::getContainer()->get('.zenstruck_foundry.configuration'); + + Object1Factory::createOne(); + } + + #[Test] + #[RequiresPhpunitExtension(FoundryExtension::class)] + #[IgnoreDeprecations] + public function using_a_story_without_factories_trait_should_throw(): void + { + $this->assertDeprecation(); + + ObjectStory::load(); + } + + /** + * We need to at least boot and shutdown Foundry to avoid unpredictable behaviors. + * + * In user land, Foundry can work without the trait, because it may have been booted in a previous test. + */ + #[Before] + public function _beforeHook(): void + { + Configuration::boot(static function(): Configuration { + return static::getContainer()->get('.zenstruck_foundry.configuration'); // @phpstan-ignore return.type + }); + } + + #[After] + public static function _shutdownFoundry(): void + { + Configuration::shutdown(); + } + + private function assertDeprecation(): void + { + $this->expectUserDeprecationMessageMatches('/In order to use Foundry correctly, you must use the trait "Zenstruck\\\\Foundry\\\\Test\\\\Factories/'); + } +} diff --git a/tests/Integration/ForceFactoriesTraitUsage/UnitTestCaseWithFactoriesTraitTest.php b/tests/Integration/ForceFactoriesTraitUsage/UnitTestCaseWithFactoriesTraitTest.php new file mode 100644 index 000000000..b1d2b59f9 --- /dev/null +++ b/tests/Integration/ForceFactoriesTraitUsage/UnitTestCaseWithFactoriesTraitTest.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\Integration\ForceFactoriesTraitUsage; + +use PHPUnit\Framework\Attributes\RequiresPhpunit; +use PHPUnit\Framework\Attributes\Test; +use PHPUnit\Framework\TestCase; +use Zenstruck\Foundry\Test\Factories; +use Zenstruck\Foundry\Tests\Fixture\Factories\Object1Factory; + +#[RequiresPhpunit('>=11.0')] +final class UnitTestCaseWithFactoriesTraitTest extends TestCase +{ + use Factories; + + #[Test] + public function should_not_throw(): void + { + Object1Factory::createOne(); + + $this->expectNotToPerformAssertions(); + } +} diff --git a/tests/Integration/ForceFactoriesTraitUsage/UnitTestCaseWithoutFactoriesTraitTest.php b/tests/Integration/ForceFactoriesTraitUsage/UnitTestCaseWithoutFactoriesTraitTest.php new file mode 100644 index 000000000..2275b1af3 --- /dev/null +++ b/tests/Integration/ForceFactoriesTraitUsage/UnitTestCaseWithoutFactoriesTraitTest.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\Tests\Integration\ForceFactoriesTraitUsage; + +use PHPUnit\Framework\Attributes\RequiresPhpunit; +use PHPUnit\Framework\Attributes\Test; +use PHPUnit\Framework\TestCase; +use Zenstruck\Foundry\Exception\FoundryNotBooted; +use Zenstruck\Foundry\Tests\Fixture\Factories\Object1Factory; + +#[RequiresPhpunit('>=11.0')] +final class UnitTestCaseWithoutFactoriesTraitTest extends TestCase +{ + #[Test] + public function should_throw(): void + { + $this->expectException(FoundryNotBooted::class); + + Object1Factory::createOne(); + } +} diff --git a/tests/Integration/InMemory/InMemoryAttributeOnMethodTest.php b/tests/Integration/InMemory/InMemoryAttributeOnMethodTest.php new file mode 100644 index 000000000..9444f67c7 --- /dev/null +++ b/tests/Integration/InMemory/InMemoryAttributeOnMethodTest.php @@ -0,0 +1,77 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Tests\Integration\InMemory; + +use Doctrine\ORM\EntityManagerInterface; +use PHPUnit\Framework\Attributes\RequiresPhpunit; +use PHPUnit\Framework\Attributes\RequiresPhpunitExtension; +use PHPUnit\Framework\Attributes\Test; +use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; +use Zenstruck\Foundry\InMemory\AsInMemoryTest; +use Zenstruck\Foundry\PHPUnit\FoundryExtension; +use Zenstruck\Foundry\Test\Factories; +use Zenstruck\Foundry\Test\ResetDatabase; +use Zenstruck\Foundry\Tests\Fixture\Entity\Address; +use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\Address\AddressFactory; +use Zenstruck\Foundry\Tests\Integration\RequiresORM; + +/** + * @author Nicolas PHILIPPE + * @requires PHPUnit >=11.4 + */ +#[RequiresPhpunit('>=11.4')] +#[RequiresPhpunitExtension(FoundryExtension::class)] +final class InMemoryAttributeOnMethodTest extends KernelTestCase +{ + use Factories; + use RequiresORM; + use ResetDatabase; + + private EntityManagerInterface $entityManager; + + protected function setUp(): void + { + $this->entityManager = self::getContainer()->get(EntityManagerInterface::class); // @phpstan-ignore assign.propertyType + } + + /** + * @test + */ + #[Test] + #[AsInMemoryTest] + public function create_one_does_not_persist_in_database(): void + { + $address = AddressFactory::createOne(); + self::assertInstanceOf(Address::class, $address); + + self::assertSame(0, $this->entityManager->getRepository(Address::class)->count([])); + + // id is autogenerated from the db, then it should be null + self::assertNull($address->id); + } + + /** + * @test + */ + #[Test] + public function not_in_memory_test(): void + { + $address = AddressFactory::createOne(); + self::assertInstanceOf(Address::class, $address); + + self::assertSame(1, $this->entityManager->getRepository(Address::class)->count([])); + + self::assertNotNull($address->id); + } +} diff --git a/tests/Integration/InMemory/InMemoryTest.php b/tests/Integration/InMemory/InMemoryTest.php new file mode 100644 index 000000000..942b7d071 --- /dev/null +++ b/tests/Integration/InMemory/InMemoryTest.php @@ -0,0 +1,162 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Tests\Integration\InMemory; + +use Doctrine\ORM\EntityManagerInterface; +use PHPUnit\Framework\Attributes\RequiresPhpunit; +use PHPUnit\Framework\Attributes\RequiresPhpunitExtension; +use PHPUnit\Framework\Attributes\Test; +use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; +use Zenstruck\Foundry\InMemory\AsInMemoryTest; +use Zenstruck\Foundry\PHPUnit\FoundryExtension; +use Zenstruck\Foundry\Test\Factories; +use Zenstruck\Foundry\Test\ResetDatabase; +use Zenstruck\Foundry\Tests\Fixture\Entity\Address; +use Zenstruck\Foundry\Tests\Fixture\Entity\Category; +use Zenstruck\Foundry\Tests\Fixture\Entity\Contact; +use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\Address\AddressFactory; +use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\Category\CategoryFactory; +use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\Contact\ContactFactory; +use Zenstruck\Foundry\Tests\Fixture\InMemory\InMemoryAddressRepository; +use Zenstruck\Foundry\Tests\Fixture\InMemory\InMemoryContactRepository; +use Zenstruck\Foundry\Tests\Integration\RequiresORM; + +/** + * @author Nicolas PHILIPPE + * @requires PHPUnit >=11.4 + */ +#[RequiresPhpunit('>=11.4')] +#[RequiresPhpunitExtension(FoundryExtension::class)] +#[AsInMemoryTest] +final class InMemoryTest extends KernelTestCase +{ + use Factories; + use RequiresORM; + use ResetDatabase; + + private InMemoryAddressRepository $addressRepository; + private InMemoryContactRepository $contactRepository; + + private EntityManagerInterface $entityManager; + + protected function setUp(): void + { + $this->addressRepository = self::getContainer()->get(InMemoryAddressRepository::class); // @phpstan-ignore assign.propertyType + $this->contactRepository = self::getContainer()->get(InMemoryContactRepository::class); // @phpstan-ignore assign.propertyType + + $this->entityManager = self::getContainer()->get(EntityManagerInterface::class); // @phpstan-ignore assign.propertyType + } + + /** + * @test + */ + #[Test] + public function create_one_does_not_persist_in_database(): void + { + $address = AddressFactory::createOne(); + self::assertInstanceOf(Address::class, $address); + + self::assertSame(0, $this->entityManager->getRepository(Address::class)->count([])); + + // id is autogenerated from the db, then it should be null + self::assertNull($address->id); + } + + /** + * @test + */ + #[Test] + public function create_many_does_not_persist_in_database(): void + { + $addresses = AddressFactory::createMany(2); + self::assertContainsOnlyInstancesOf(Address::class, $addresses); + + self::assertSame(0, $this->entityManager->getRepository(Address::class)->count([])); + + foreach ($addresses as $address) { + // id is autogenerated from the db, then it should be null + self::assertNull($address->id); + } + } + + /** + * @test + */ + #[Test] + public function object_should_be_accessible_from_in_memory_repository(): void + { + $address = AddressFactory::createOne(); + + self::assertSame([$address], $this->addressRepository->_all()); + } + + /** + * @test + */ + #[Test] + public function nested_objects_should_be_accessible_from_their_respective_repository(): void + { + $contact = ContactFactory::createOne(); + + self::assertSame([$contact], $this->contactRepository->_all()); + self::assertSame([$contact->getAddress()], $this->addressRepository->_all()); + + self::assertSame(0, $this->entityManager->getRepository(Address::class)->count([])); + self::assertSame(0, $this->entityManager->getRepository(Contact::class)->count([])); + } + + /** + * @test + */ + #[Test] + public function can_use_generic_repository(): void + { + $category = CategoryFactory::createOne([ + 'contacts' => ContactFactory::new()->many(2), + ]); + + self::assertSame(0, $this->entityManager->getRepository(Category::class)->count([])); + + self::assertSame($this->contactRepository->_all(), $category->getContacts()->toArray()); + } + + /** + * @test + */ + #[Test] + public function one_to_many(): void + { + $category = CategoryFactory::createOne([ + 'contacts' => ContactFactory::new()->many(2), + ]); + + $this->assertCount(2, $category->getContacts()); + + foreach ($category->getContacts() as $contact) { + $this->assertSame($category, $contact->getCategory()); + } + } + + /** + * @test + */ + #[Test] + public function inversed_one_to_one(): void + { + $address = AddressFactory::createOne(['contact' => ContactFactory::new()]); + + self::assertNotNull($address->getContact()); + self::assertSame($address, $address->getContact()->getAddress()); + } +} diff --git a/tests/Integration/Maker/MakeFactoryTest.php b/tests/Integration/Maker/MakeFactoryTest.php index 4e4b741eb..39e4a7a47 100644 --- a/tests/Integration/Maker/MakeFactoryTest.php +++ b/tests/Integration/Maker/MakeFactoryTest.php @@ -17,12 +17,13 @@ use Zenstruck\Foundry\Maker\Factory\FactoryGenerator; use Zenstruck\Foundry\Tests\Fixture\Document\GenericDocument; use Zenstruck\Foundry\Tests\Fixture\Document\WithEmbeddableDocument; -use Zenstruck\Foundry\Tests\Fixture\Entity\Category\StandardCategory; -use Zenstruck\Foundry\Tests\Fixture\Entity\Contact\StandardContact; +use Zenstruck\Foundry\Tests\Fixture\Entity\Category; +use Zenstruck\Foundry\Tests\Fixture\Entity\Contact; use Zenstruck\Foundry\Tests\Fixture\Entity\GenericEntity; use Zenstruck\Foundry\Tests\Fixture\Entity\WithEmbeddableEntity; use Zenstruck\Foundry\Tests\Fixture\Object1; use Zenstruck\Foundry\Tests\Fixture\ObjectWithEnum; +use Zenstruck\Foundry\Tests\Fixture\ObjectWithNonWriteable; /** * @author Kevin Bond @@ -66,13 +67,13 @@ public function can_create_factory(): void $tester = $this->makeFactoryCommandTester(); - $tester->execute(['class' => StandardCategory::class]); + $tester->execute(['class' => Category::class]); $output = $tester->getDisplay(); $this->assertStringContainsString('Note: pass --test if you want to generate factories in your tests/ directory', $output); - $this->assertFileFromMakerSameAsExpectedFile(self::tempFile('src/Factory/Category/StandardCategoryFactory.php')); + $this->assertFileFromMakerSameAsExpectedFile(self::tempFile('src/Factory/CategoryFactory.php')); } /** @@ -87,19 +88,19 @@ public function can_create_factory_interactively(): void $tester = $this->makeFactoryCommandTester(); $tester->setInputs([ - StandardContact::class, // which class to create a factory for? - 'yes', // should create PostFactory for StandardContact::$address? + Contact::class, // which class to create a factory for? + 'yes', // should create PostFactory for Contact::$address? ]); $tester->execute([], ['interactive' => true]); $output = $tester->getDisplay(); $this->assertStringContainsString( - 'A factory for class "Zenstruck\Foundry\Tests\Fixture\Entity\Address\StandardAddress" is missing for field StandardContact::$address. Do you want to create it?', + 'A factory for class "Zenstruck\Foundry\Tests\Fixture\Entity\Address" is missing for field Contact::$address. Do you want to create it?', $output, ); - $this->assertFileExists(self::tempFile('src/Factory/Address/StandardAddressFactory.php')); - $this->assertFileFromMakerSameAsExpectedFile(self::tempFile('src/Factory/Contact/StandardContactFactory.php')); + $this->assertFileExists(self::tempFile('src/Factory/AddressFactory.php')); + $this->assertFileFromMakerSameAsExpectedFile(self::tempFile('src/Factory/ContactFactory.php')); } /** @@ -113,9 +114,9 @@ public function can_create_factory_in_test_dir(): void $tester = $this->makeFactoryCommandTester(); - $tester->execute(['class' => StandardCategory::class, '--test' => true]); + $tester->execute(['class' => Category::class, '--test' => true]); - $this->assertFileExists(self::tempFile('tests/Factory/Category/StandardCategoryFactory.php')); + $this->assertFileExists(self::tempFile('tests/Factory/CategoryFactory.php')); } /** @@ -132,9 +133,9 @@ public function can_create_factory_with_static_analysis_annotations(string $scaT $tester = $this->makeFactoryCommandTester(); - $tester->execute(['class' => StandardCategory::class, '--test' => true, '--with-phpdoc' => true]); + $tester->execute(['class' => Category::class, '--test' => true, '--with-phpdoc' => true]); - $this->assertFileFromMakerSameAsExpectedFile(self::tempFile('tests/Factory/Category/StandardCategoryFactory.php')); + $this->assertFileFromMakerSameAsExpectedFile(self::tempFile('tests/Factory/CategoryFactory.php')); } /** @@ -421,14 +422,38 @@ public function can_create_factory_with_default_enum(): void $this->assertFileFromMakerSameAsExpectedFile(self::tempFile('src/Factory/ObjectWithEnumFactory.php')); } + /** + * @test + */ + public function does_not_initialize_non_settable(): void + { + $tester = $this->makeFactoryCommandTester(); + + $tester->execute(['class' => ObjectWithNonWriteable::class, '--no-persistence' => true]); + + $this->assertFileFromMakerSameAsExpectedFile(self::tempFile('src/Factory/ObjectWithNonWriteableFactory.php')); + } + + /** + * @test + */ + public function does_force_initialization_of_non_settable_with_always_force(): void + { + $tester = $this->makeFactoryCommandTester(['environment' => 'always_force']); + + $tester->execute(['class' => ObjectWithNonWriteable::class, '--no-persistence' => true]); + + $this->assertFileFromMakerSameAsExpectedFile(self::tempFile('src/Factory/ObjectWithNonWriteableFactory.php')); + } + private function emulateSCAToolEnabled(string $scaToolFilePath): void { \mkdir(\dirname($scaToolFilePath), 0777, true); \touch($scaToolFilePath); } - private function makeFactoryCommandTester(): CommandTester + private function makeFactoryCommandTester(array $options = []): CommandTester { - return new CommandTester((new Application(self::bootKernel()))->find('make:factory')); + return new CommandTester((new Application(self::bootKernel($options)))->find('make:factory')); } } diff --git a/tests/Integration/Maker/MakeStoryTest.php b/tests/Integration/Maker/MakeStoryTest.php index 072bc932c..81f214088 100644 --- a/tests/Integration/Maker/MakeStoryTest.php +++ b/tests/Integration/Maker/MakeStoryTest.php @@ -17,7 +17,6 @@ /** * @author Kevin Bond * @group maker - * @requires PHP 8.1 */ final class MakeStoryTest extends MakerTestCase { diff --git a/tests/Integration/Maker/MakerTestCase.php b/tests/Integration/Maker/MakerTestCase.php index b2c974cf0..1e830d3d9 100644 --- a/tests/Integration/Maker/MakerTestCase.php +++ b/tests/Integration/Maker/MakerTestCase.php @@ -18,7 +18,6 @@ /** * @author Kevin Bond * @group maker - * @requires PHP 8.1 */ abstract class MakerTestCase extends KernelTestCase { diff --git a/tests/Integration/Mongo/GenericDocumentFactoryTest.php b/tests/Integration/Mongo/GenericDocumentFactoryTest.php index ab6f5a606..df7be982f 100644 --- a/tests/Integration/Mongo/GenericDocumentFactoryTest.php +++ b/tests/Integration/Mongo/GenericDocumentFactoryTest.php @@ -12,7 +12,6 @@ namespace Zenstruck\Foundry\Tests\Integration\Mongo; use Zenstruck\Foundry\Tests\Fixture\Factories\Document\GenericDocumentFactory; -use Zenstruck\Foundry\Tests\Fixture\Factories\GenericModelFactory; use Zenstruck\Foundry\Tests\Integration\Persistence\GenericFactoryTestCase; use Zenstruck\Foundry\Tests\Integration\RequiresMongo; @@ -23,7 +22,7 @@ final class GenericDocumentFactoryTest extends GenericFactoryTestCase { use RequiresMongo; - protected function factory(): GenericModelFactory + protected static function factory(): GenericDocumentFactory { return GenericDocumentFactory::new(); } diff --git a/tests/Integration/Mongo/GenericDocumentProxyFactoryTest.php b/tests/Integration/Mongo/GenericDocumentProxyFactoryTest.php index f3e623657..05d2d9f85 100644 --- a/tests/Integration/Mongo/GenericDocumentProxyFactoryTest.php +++ b/tests/Integration/Mongo/GenericDocumentProxyFactoryTest.php @@ -26,7 +26,7 @@ final class GenericDocumentProxyFactoryTest extends GenericProxyFactoryTestCase { use RequiresMongo; - protected function factory(): PersistentProxyObjectFactory + protected static function factory(): GenericProxyDocumentFactory { return GenericProxyDocumentFactory::new(); } diff --git a/tests/Integration/Mongo/PersistenceManagerTest.php b/tests/Integration/Mongo/PersistenceManagerTest.php new file mode 100644 index 000000000..b1b87da9f --- /dev/null +++ b/tests/Integration/Mongo/PersistenceManagerTest.php @@ -0,0 +1,35 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Tests\Integration\Mongo; + +use Doctrine\ODM\MongoDB\DocumentManager; +use Doctrine\Persistence\ObjectManager; +use Zenstruck\Foundry\Tests\Fixture\Document\DocumentWithUid; +use Zenstruck\Foundry\Tests\Integration\Persistence\PersistenceManagerTestCase; +use Zenstruck\Foundry\Tests\Integration\RequiresMongo; + +final class PersistenceManagerTest extends PersistenceManagerTestCase +{ + use RequiresMongo; + + protected static function createObject(): object + { + return new DocumentWithUid(); + } + + protected static function objectManager(): ObjectManager + { + return self::getContainer()->get(DocumentManager::class); // @phpstan-ignore return.type + } +} diff --git a/tests/Integration/ORM/CascadeEntityFactoryRelationshipTest.php b/tests/Integration/ORM/CascadeEntityFactoryRelationshipTest.php deleted file mode 100644 index ec00ef40f..000000000 --- a/tests/Integration/ORM/CascadeEntityFactoryRelationshipTest.php +++ /dev/null @@ -1,116 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Zenstruck\Foundry\Tests\Integration\ORM; - -use Zenstruck\Foundry\Persistence\PersistentObjectFactory; -use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\Address\CascadeAddressFactory; -use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\Category\CascadeCategoryFactory; -use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\Contact\CascadeContactFactory; -use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\Tag\CascadeTagFactory; - -/** - * @author Kevin Bond - */ -final class CascadeEntityFactoryRelationshipTest extends EntityFactoryRelationshipTestCase -{ - /** - * @test - */ - public function ensure_to_one_cascade_relations_are_not_pre_persisted(): void - { - $contact = $this->contactFactory() - ->afterInstantiate(function() { - $this->categoryFactory()::repository()->assert()->empty(); - $this->addressFactory()::repository()->assert()->empty(); - $this->tagFactory()::repository()->assert()->empty(); - }) - ->create([ - 'tags' => $this->tagFactory()->many(3), - 'category' => $this->categoryFactory(), - ]) - ; - - $this->assertNotNull($contact->getCategory()?->id); - $this->assertNotNull($contact->getAddress()->id); - $this->assertCount(3, $contact->getTags()); - - foreach ($contact->getTags() as $tag) { - $this->assertNotNull($tag->id); - } - } - - /** - * @test - */ - public function ensure_many_to_many_cascade_relations_are_not_pre_persisted(): void - { - $tag = $this->tagFactory() - ->afterInstantiate(function() { - $this->categoryFactory()::repository()->assert()->empty(); - $this->addressFactory()::repository()->assert()->empty(); - $this->contactFactory()::repository()->assert()->empty(); - }) - ->create([ - 'contacts' => $this->contactFactory()->many(3), - ]) - ; - - $this->assertCount(3, $tag->getContacts()); - - foreach ($tag->getContacts() as $contact) { - $this->assertNotNull($contact->id); - } - } - - /** - * @test - */ - public function ensure_one_to_many_cascade_relations_are_not_pre_persisted(): void - { - $category = $this->categoryFactory() - ->afterInstantiate(function() { - $this->contactFactory()::repository()->assert()->empty(); - $this->addressFactory()::repository()->assert()->empty(); - $this->tagFactory()::repository()->assert()->empty(); - }) - ->create([ - 'contacts' => $this->contactFactory()->many(3), - ]) - ; - - $this->assertCount(3, $category->getContacts()); - - foreach ($category->getContacts() as $contact) { - $this->assertNotNull($contact->id); - } - } - - protected function contactFactory(): PersistentObjectFactory - { - return CascadeContactFactory::new(); // @phpstan-ignore return.type - } - - protected function categoryFactory(): PersistentObjectFactory - { - return CascadeCategoryFactory::new(); // @phpstan-ignore return.type - } - - protected function tagFactory(): PersistentObjectFactory - { - return CascadeTagFactory::new(); // @phpstan-ignore return.type - } - - protected function addressFactory(): PersistentObjectFactory - { - return CascadeAddressFactory::new(); // @phpstan-ignore return.type - } -} diff --git a/tests/Integration/ORM/EdgeCasesRelationshipTest.php b/tests/Integration/ORM/EdgeCasesRelationshipTest.php index 621fafb96..5605c2f1b 100644 --- a/tests/Integration/ORM/EdgeCasesRelationshipTest.php +++ b/tests/Integration/ORM/EdgeCasesRelationshipTest.php @@ -13,97 +13,149 @@ namespace Zenstruck\Foundry\Tests\Integration\ORM; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\RequiresPhpunit; +use PHPUnit\Framework\Attributes\Test; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; -use Zenstruck\Foundry\Persistence\PersistentObjectFactory; -use Zenstruck\Foundry\Persistence\Proxy; use Zenstruck\Foundry\Test\Factories; use Zenstruck\Foundry\Test\ResetDatabase; -use Zenstruck\Foundry\Tests\Fixture\Entity\EdgeCases\RelationshipWithGlobalEntity; +use Zenstruck\Foundry\Tests\Fixture\DoctrineCascadeRelationship\ChangesEntityRelationshipCascadePersist; +use Zenstruck\Foundry\Tests\Fixture\DoctrineCascadeRelationship\UsingRelationships; +use Zenstruck\Foundry\Tests\Fixture\Entity\EdgeCases\IndexedOneToMany; +use Zenstruck\Foundry\Tests\Fixture\Entity\EdgeCases\InversedOneToOneWithNonNullableOwning; +use Zenstruck\Foundry\Tests\Fixture\Entity\EdgeCases\InversedOneToOneWithOneToMany; +use Zenstruck\Foundry\Tests\Fixture\Entity\EdgeCases\InversedOneToOneWithSetter; +use Zenstruck\Foundry\Tests\Fixture\Entity\EdgeCases\ManyToOneToSelfReferencing; use Zenstruck\Foundry\Tests\Fixture\Entity\EdgeCases\RichDomainMandatoryRelationship; -use Zenstruck\Foundry\Tests\Fixture\Entity\GlobalEntity; use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\EdgeCases\MultipleMandatoryRelationshipToSameEntity; -use Zenstruck\Foundry\Tests\Fixture\Stories\GlobalStory; use Zenstruck\Foundry\Tests\Integration\RequiresORM; -use function Zenstruck\Foundry\Persistence\flush_after; use function Zenstruck\Foundry\Persistence\persistent_factory; -use function Zenstruck\Foundry\Persistence\proxy_factory; /** * @author Nicolas PHILIPPE */ final class EdgeCasesRelationshipTest extends KernelTestCase { - use Factories, RequiresORM, ResetDatabase; + use ChangesEntityRelationshipCascadePersist, Factories, RequiresORM, ResetDatabase; + + /** @test */ + #[Test] + #[DataProvider('provideCascadeRelationshipsCombinations')] + #[UsingRelationships(RichDomainMandatoryRelationship\OwningSide::class, ['main'])] + #[RequiresPhpunit('>=11.4')] + public function inversed_relationship_mandatory(): void + { + $owningSideEntityFactory = persistent_factory(RichDomainMandatoryRelationship\OwningSide::class); + $inversedSideEntityFactory = persistent_factory(RichDomainMandatoryRelationship\InversedSide::class); - /** - * @test - * @param PersistentObjectFactory $relationshipWithGlobalEntityFactory - * @dataProvider relationshipWithGlobalEntityFactoryProvider - */ - public function it_can_use_flush_after_and_entity_from_global_state(PersistentObjectFactory $relationshipWithGlobalEntityFactory, bool $asProxy): void + $inversedSideEntity = $inversedSideEntityFactory->create([ + 'relations' => $owningSideEntityFactory->many(2), + ]); + + $this->assertCount(2, $inversedSideEntity->getRelations()); + $owningSideEntityFactory::assert()->count(2); + $inversedSideEntityFactory::assert()->count(1); + } + + /** @test */ + #[Test] + #[DataProvider('provideCascadeRelationshipsCombinations')] + #[UsingRelationships(InversedOneToOneWithNonNullableOwning\OwningSide::class, ['inverseSide'])] + #[RequiresPhpunit('>=11.4')] + public function inverse_one_to_one_with_non_nullable_inverse_side(): void { - $globalEntitiesCount = persistent_factory(GlobalEntity::class)::repository()->count(); + $owningSideFactory = persistent_factory(InversedOneToOneWithNonNullableOwning\OwningSide::class); + $inverseSideFactory = persistent_factory(InversedOneToOneWithNonNullableOwning\InverseSide::class); + + $inverseSide = $inverseSideFactory->create(['owningSide' => $owningSideFactory]); + + $owningSideFactory::assert()->count(1); + $inverseSideFactory::assert()->count(1); - flush_after(function() use ($relationshipWithGlobalEntityFactory, $asProxy) { - $globalEntity = $asProxy ? GlobalStory::globalEntityProxy() : GlobalStory::globalEntity(); - self::assertSame($asProxy, $globalEntity instanceof Proxy); + self::assertSame($inverseSide, $inverseSide->owningSide->inverseSide); + } - $relationshipWithGlobalEntityFactory->create(['globalEntity' => $globalEntity]); - }); + /** @test */ + #[Test] + #[DataProvider('provideCascadeRelationshipsCombinations')] + #[UsingRelationships(InversedOneToOneWithSetter\OwningSide::class, ['inverseSide'])] + #[RequiresPhpunit('>=11.4')] + public function inverse_one_to_one_with_both_nullable(): void + { + $owningSideFactory = persistent_factory(InversedOneToOneWithSetter\OwningSide::class); + $inverseSideFactory = persistent_factory(InversedOneToOneWithSetter\InverseSide::class); - // assert no extra GlobalEntity have been created - persistent_factory(GlobalEntity::class)::assert()->count($globalEntitiesCount); + $inverseSide = $inverseSideFactory->create(['owningSide' => $owningSideFactory]); - $relationshipWithGlobalEntityFactory::assert()->count(1); + $owningSideFactory::assert()->count(1); + $inverseSideFactory::assert()->count(1); - $entity = $relationshipWithGlobalEntityFactory::repository()->first(); - self::assertSame(GlobalStory::globalEntity(), $entity?->getGlobalEntity()); + self::assertSame($inverseSide, $inverseSide->getOwningSide()?->inverseSide); } - public static function relationshipWithGlobalEntityFactoryProvider(): iterable + /** @test */ + #[Test] + #[DataProvider('provideCascadeRelationshipsCombinations')] + #[UsingRelationships(InversedOneToOneWithOneToMany\OwningSide::class, ['inverseSide'])] + #[UsingRelationships(InversedOneToOneWithOneToMany\Item::class, ['owningSide'])] + #[RequiresPhpunit('^11.4')] + public function inverse_one_to_one_with_one_to_many(): void { - yield [persistent_factory(RelationshipWithGlobalEntity\StandardRelationshipWithGlobalEntity::class), false]; - yield [persistent_factory(RelationshipWithGlobalEntity\CascadeRelationshipWithGlobalEntity::class), false]; - yield [persistent_factory(RelationshipWithGlobalEntity\StandardRelationshipWithGlobalEntity::class), true]; - yield [persistent_factory(RelationshipWithGlobalEntity\CascadeRelationshipWithGlobalEntity::class), true]; + $inverseSideFactory = persistent_factory(InversedOneToOneWithOneToMany\InverseSide::class); + $owningSideFactory = persistent_factory(InversedOneToOneWithOneToMany\OwningSide::class); + $itemFactory = persistent_factory(InversedOneToOneWithOneToMany\Item::class) + // "with()" attribute emulates what would be found in the "defaults()" method in a real factory + ->with(['owningSide' => $owningSideFactory]); + + $inverseSide = $inverseSideFactory->create([ + 'owningSide' => $owningSideFactory->with([ + 'items' => $itemFactory->many(2), + ]), + ]); + + $owningSideFactory::assert()->count(1); + $inverseSideFactory::assert()->count(1); + $itemFactory::assert()->count(2); + + self::assertSame($inverseSide, $inverseSide->getOwningSide()?->inverseSide); + self::assertCount(2, $inverseSide->getOwningSide()->getItems()); } /** * @test - * @param PersistentObjectFactory $inversedSideEntityFactory - * @param PersistentObjectFactory $owningSideEntityFactory - * @dataProvider richDomainMandatoryRelationshipFactoryProvider */ - public function inversed_relationship_mandatory(PersistentObjectFactory $inversedSideEntityFactory, PersistentObjectFactory $owningSideEntityFactory): void + public function many_to_many_to_self_referencing_inverse_side(): void { - $inversedSideEntity = $inversedSideEntityFactory->create([ - 'relations' => $owningSideEntityFactory->many(2), - ]); + $owningSideFactory = persistent_factory(ManyToOneToSelfReferencing\OwningSide::class); + $inverseSideFactory = persistent_factory(ManyToOneToSelfReferencing\SelfReferencingInverseSide::class); - $this->assertCount(2, $inversedSideEntity->getRelations()); - $owningSideEntityFactory::assert()->count(2); - $inversedSideEntityFactory::assert()->count(1); + $owningSideFactory->create(['inverseSide' => $inverseSideFactory]); + + $owningSideFactory::assert()->count(1); + $inverseSideFactory::assert()->count(1); } - public static function richDomainMandatoryRelationshipFactoryProvider(): iterable + /** @test */ + #[Test] + #[DataProvider('provideCascadeRelationshipsCombinations')] + #[UsingRelationships(IndexedOneToMany\ParentEntity::class, ['items'])] + #[RequiresPhpunit('^11.4')] + public function indexed_one_to_many(): void { - yield [ - persistent_factory(RichDomainMandatoryRelationship\StandardInversedSideEntity::class), - persistent_factory(RichDomainMandatoryRelationship\StandardOwningSideEntity::class), - ]; - yield [ - proxy_factory(RichDomainMandatoryRelationship\CascadeInversedSideEntity::class), - proxy_factory(RichDomainMandatoryRelationship\CascadeOwningSideEntity::class), - ]; - yield [ - persistent_factory(RichDomainMandatoryRelationship\StandardInversedSideEntity::class), - persistent_factory(RichDomainMandatoryRelationship\StandardOwningSideEntity::class), - ]; - yield [ - proxy_factory(RichDomainMandatoryRelationship\CascadeInversedSideEntity::class), - proxy_factory(RichDomainMandatoryRelationship\CascadeOwningSideEntity::class), - ]; + $parentFactory = persistent_factory(IndexedOneToMany\ParentEntity::class); + $childFactory = persistent_factory(IndexedOneToMany\Child::class); + + $parent = $parentFactory->create( + [ + 'items' => $childFactory->with(['language' => 'en', 'parent' => $parentFactory])->many(1), + ] + ); + + $parentFactory::assert()->count(1); + $childFactory::assert()->count(1); + + self::assertNotNull($parent->getItems()->get('en')); // @phpstan-ignore argument.type } /** diff --git a/tests/Integration/ORM/EntityFactoryRelationshipTestCase.php b/tests/Integration/ORM/EntityFactoryRelationshipTestCase.php deleted file mode 100644 index a786f217d..000000000 --- a/tests/Integration/ORM/EntityFactoryRelationshipTestCase.php +++ /dev/null @@ -1,323 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Zenstruck\Foundry\Tests\Integration\ORM; - -use Doctrine\ORM\EntityManagerInterface; -use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; -use Zenstruck\Foundry\Object\Instantiator; -use Zenstruck\Foundry\Persistence\PersistentObjectFactory; -use Zenstruck\Foundry\Test\Factories; -use Zenstruck\Foundry\Test\ResetDatabase; -use Zenstruck\Foundry\Tests\Fixture\Entity\Address; -use Zenstruck\Foundry\Tests\Fixture\Entity\Category; -use Zenstruck\Foundry\Tests\Fixture\Entity\Contact; -use Zenstruck\Foundry\Tests\Fixture\Entity\Tag; -use Zenstruck\Foundry\Tests\Integration\RequiresORM; - -use function Zenstruck\Foundry\Persistence\unproxy; - -/** - * @author Kevin Bond - */ -abstract class EntityFactoryRelationshipTestCase extends KernelTestCase -{ - use Factories, RequiresORM, ResetDatabase; - - /** - * @test - */ - public function many_to_one(): void - { - $contact = $this->contactFactory()::createOne([ - 'category' => $this->categoryFactory(), - ]); - - $this->contactFactory()::repository()->assert()->count(1); - $this->categoryFactory()::repository()->assert()->count(1); - - $this->assertNotNull($contact->id); - $this->assertNotNull($contact->getCategory()?->id); - } - - /** - * @test - */ - public function disabling_persistence_cascades_to_children(): void - { - $contact = $this->contactFactory()->withoutPersisting()->create([ - 'tags' => $this->tagFactory()->many(3), - 'category' => $this->categoryFactory(), - ]); - - $this->contactFactory()::repository()->assert()->empty(); - $this->categoryFactory()::repository()->assert()->empty(); - $this->tagFactory()::repository()->assert()->empty(); - $this->addressFactory()::repository()->assert()->empty(); - - $this->assertNull($contact->id); - $this->assertNull($contact->getCategory()?->id); - $this->assertNull($contact->getAddress()->id); - $this->assertCount(3, $contact->getTags()); - - foreach ($contact->getTags() as $tag) { - $this->assertNull($tag->id); - } - - $category = $this->categoryFactory()->withoutPersisting()->create([ - 'contacts' => $this->contactFactory()->many(3), - ]); - - $this->contactFactory()::repository()->assert()->empty(); - $this->categoryFactory()::repository()->assert()->empty(); - - $this->assertNull($category->id); - $this->assertCount(3, $category->getContacts()); - - foreach ($category->getContacts() as $contact) { - $this->assertSame($category->getName(), $contact->getCategory()?->getName()); - } - } - - /** - * @test - */ - public function one_to_many(): void - { - $category = $this->categoryFactory()::createOne([ - 'contacts' => $this->contactFactory()->many(3), - ]); - - $this->contactFactory()::repository()->assert()->count(3); - $this->categoryFactory()::repository()->assert()->count(1); - $this->assertNotNull($category->id); - $this->assertCount(3, $category->getContacts()); - - foreach ($category->getContacts() as $contact) { - $this->assertSame($category->id, $contact->getCategory()?->id); - } - } - - /** - * @test - */ - public function many_to_many_owning(): void - { - $tag = $this->tagFactory()::createOne([ - 'contacts' => $this->contactFactory()->many(3), - ]); - - $this->contactFactory()::repository()->assert()->count(3); - $this->tagFactory()::repository()->assert()->count(1); - $this->assertNotNull($tag->id); - - foreach ($tag->getContacts() as $contact) { - $this->assertSame($tag->id, $contact->getTags()[0]?->id); - } - } - - /** - * @test - */ - public function many_to_many_owning_as_array(): void - { - $tag = $this->tagFactory()::createOne([ - 'contacts' => [$this->contactFactory(), $this->contactFactory(), $this->contactFactory()], - ]); - - $this->contactFactory()::repository()->assert()->count(3); - $this->tagFactory()::repository()->assert()->count(1); - $this->assertNotNull($tag->id); - - foreach ($tag->getContacts() as $contact) { - $this->assertSame($tag->id, $contact->getTags()[0]?->id); - } - } - - /** - * @test - */ - public function many_to_many_inverse(): void - { - $contact = $this->contactFactory()::createOne([ - 'tags' => $this->tagFactory()->many(3), - ]); - - $this->contactFactory()::repository()->assert()->count(1); - $this->tagFactory()::repository()->assert()->count(3); - $this->assertNotNull($contact->id); - - foreach ($contact->getTags() as $tag) { - $this->assertTrue($contact->getTags()->contains($tag)); - $this->assertNotNull($tag->id); - } - } - - /** - * @test - */ - public function one_to_one_owning(): void - { - $contact = $this->contactFactory()::createOne(); - - $this->contactFactory()::repository()->assert()->count(1); - $this->addressFactory()::repository()->assert()->count(1); - - $this->assertNotNull($contact->id); - $this->assertNotNull($contact->getAddress()->id); - } - - /** - * @test - */ - public function one_to_one_inverse(): void - { - $this->markTestSkipped('Not supported. Should it be?'); - } - - /** - * @test - */ - public function many_to_one_unmanaged_raw_entity(): void - { - $address = unproxy($this->addressFactory()->create(['city' => 'Some city'])); - - /** @var EntityManagerInterface $em */ - $em = self::getContainer()->get(EntityManagerInterface::class); - $em->clear(); - - $contact = $this->contactFactory()->create(['address' => $address]); - - $this->assertSame('Some city', $contact->getAddress()->getCity()); - } - - /** - * @test - */ - public function inverse_one_to_many_relationship(): void - { - $this->categoryFactory()::assert()->count(0); - $this->contactFactory()::assert()->count(0); - - $this->categoryFactory()->create([ - 'contacts' => [ - $this->contactFactory(), - $this->contactFactory()->create(), - ], - ]); - - $this->categoryFactory()::assert()->count(1); - $this->contactFactory()::assert()->count(2); - } - - /** - * @test - */ - public function one_to_many_with_two_relationships_same_entity(): void - { - $category = $this->categoryFactory()->create([ - 'contacts' => $this->contactFactory()->many(4), - 'secondaryContacts' => $this->contactFactory()->many(4), - ]); - - $this->assertCount(4, $category->getContacts()); - $this->assertCount(4, $category->getSecondaryContacts()); - $this->contactFactory()::assert()->count(8); - $this->categoryFactory()::assert()->count(1); - } - - /** - * @test - */ - public function inverse_many_to_many_with_two_relationships_same_entity(): void - { - $this->tagFactory()::assert()->count(0); - - $tag = $this->tagFactory()->create([ - 'contacts' => $this->contactFactory()->many(3), - 'secondaryContacts' => $this->contactFactory()->many(3), - ]); - - $this->assertCount(3, $tag->getContacts()); - $this->assertCount(3, $tag->getSecondaryContacts()); - $this->tagFactory()::assert()->count(1); - $this->contactFactory()::assert()->count(6); - } - - /** - * @test - */ - public function can_use_adder_as_attributes(): void - { - $category = $this->categoryFactory()->create([ - 'addContact' => $this->contactFactory()->with(['name' => 'foo']), - ]); - - self::assertCount(1, $category->getContacts()); - self::assertSame('foo', $category->getContacts()[0]?->getName()); - } - - /** - * @test - */ - public function one_to_many_with_two_relationships_same_entity_and_adders(): void - { - $category = $this->categoryFactory()->create([ - 'addContact' => $this->contactFactory(), - 'addSecondaryContact' => $this->contactFactory(), - ]); - - $this->assertCount(1, $category->getContacts()); - $this->assertCount(1, $category->getSecondaryContacts()); - $this->contactFactory()::assert()->count(2); - $this->categoryFactory()::assert()->count(1); - } - - /** - * @test - */ - public function forced_one_to_many_with_doctrine_collection_type(): void - { - $category = $this->categoryFactory() - ->instantiateWith(Instantiator::withConstructor()->alwaysForce()) - ->create([ - 'contacts' => $this->contactFactory()->many(2), - ]) - ; - - self::assertCount(2, $category->getContacts()); - foreach ($category->getContacts() as $contact) { - self::assertSame(unproxy($category), $contact->getCategory()); - } - $this->contactFactory()::assert()->count(2); - $this->categoryFactory()::assert()->count(1); - } - - /** - * @return PersistentObjectFactory - */ - abstract protected function contactFactory(): PersistentObjectFactory; - - /** - * @return PersistentObjectFactory - */ - abstract protected function categoryFactory(): PersistentObjectFactory; - - /** - * @return PersistentObjectFactory - */ - abstract protected function tagFactory(): PersistentObjectFactory; - - /** - * @return PersistentObjectFactory
- */ - abstract protected function addressFactory(): PersistentObjectFactory; -} diff --git a/tests/Integration/ORM/EntityRelationship/EntityFactoryRelationshipTestCase.php b/tests/Integration/ORM/EntityRelationship/EntityFactoryRelationshipTestCase.php new file mode 100644 index 000000000..8ec50a06c --- /dev/null +++ b/tests/Integration/ORM/EntityRelationship/EntityFactoryRelationshipTestCase.php @@ -0,0 +1,473 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Tests\Integration\ORM\EntityRelationship; + +use Doctrine\ORM\EntityManagerInterface; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\RequiresPhpunit; +use PHPUnit\Framework\Attributes\Test; +use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; +use Zenstruck\Foundry\Factory; +use Zenstruck\Foundry\FactoryCollection; +use Zenstruck\Foundry\Object\Instantiator; +use Zenstruck\Foundry\Persistence\PersistentObjectFactory; +use Zenstruck\Foundry\Test\Factories; +use Zenstruck\Foundry\Test\ResetDatabase; +use Zenstruck\Foundry\Tests\Fixture\DoctrineCascadeRelationship\ChangesEntityRelationshipCascadePersist; +use Zenstruck\Foundry\Tests\Fixture\DoctrineCascadeRelationship\UsingRelationships; +use Zenstruck\Foundry\Tests\Fixture\Entity\Address; +use Zenstruck\Foundry\Tests\Fixture\Entity\Category; +use Zenstruck\Foundry\Tests\Fixture\Entity\Contact; +use Zenstruck\Foundry\Tests\Fixture\Entity\Tag; + +use function Zenstruck\Foundry\Persistence\refresh; +use function Zenstruck\Foundry\Persistence\unproxy; + +/** + * @author Kevin Bond + * @author Nicolas PHILIPPE + * @requires PHPUnit >=11.4 + */ +#[RequiresPhpunit('>=11.4')] +abstract class EntityFactoryRelationshipTestCase extends KernelTestCase +{ + use ChangesEntityRelationshipCascadePersist, Factories, ResetDatabase; + + /** @test */ + #[Test] + #[DataProvider('provideCascadeRelationshipsCombinations')] + #[UsingRelationships(Contact::class, ['category'])] + public function many_to_one(): void + { + $contact = static::contactFactory()->create([ + 'category' => static::categoryFactory(), + ]); + + static::contactFactory()::assert()->count(1); + static::categoryFactory()::assert()->count(1); + + $this->assertNotNull($contact->id); + $this->assertNotNull($contact->getCategory()?->id); + } + + /** @test */ + #[Test] + #[DataProvider('provideCascadeRelationshipsCombinations')] + #[UsingRelationships(Category::class, ['contacts'])] + public function one_to_many_with_factory_collection(): void + { + $this->one_to_many(static::contactFactory()->many(2)); + } + + /** @test */ + #[Test] + #[DataProvider('provideCascadeRelationshipsCombinations')] + #[UsingRelationships(Category::class, ['contacts'])] + public function one_to_many_with_array_of_factories(): void + { + $this->one_to_many([static::contactFactory(), static::contactFactory()]); + } + + /** @test */ + #[Test] + #[DataProvider('provideCascadeRelationshipsCombinations')] + #[UsingRelationships(Category::class, ['contacts'])] + public function one_to_many_with_array_of_managed_objects(): void + { + $this->one_to_many([static::contactFactoryWithoutCategory()->create(), static::contactFactoryWithoutCategory()->create()]); + } + + /** @test */ + #[Test] + #[DataProvider('provideCascadeRelationshipsCombinations')] + #[UsingRelationships(Category::class, ['contacts'])] + #[UsingRelationships(Contact::class, ['address'])] + public function inverse_one_to_many_relationship(): void + { + $category = static::categoryFactory()->create([ + 'contacts' => [ + static::contactFactoryWithoutCategory(), + static::contactFactoryWithoutCategory()->create(), + ], + ]); + + static::categoryFactory()::assert()->count(1); + static::contactFactory()::assert()->count(2); + + foreach ($category->getContacts() as $contact) { + $this->assertSame($category->id, $contact->getCategory()?->id); + } + } + + /** @test */ + #[Test] + #[DataProvider('provideCascadeRelationshipsCombinations')] + #[UsingRelationships(Tag::class, ['contacts'])] + public function many_to_many_owning(): void + { + $this->many_to_many(static::contactFactory()->many(3)); + } + + /** @test */ + #[Test] + #[DataProvider('provideCascadeRelationshipsCombinations')] + #[UsingRelationships(Tag::class, ['contacts'])] + public function many_to_many_owning_as_array(): void + { + $this->many_to_many([static::contactFactory(), static::contactFactory(), static::contactFactory()]); + } + + /** @test */ + #[Test] + #[DataProvider('provideCascadeRelationshipsCombinations')] + #[UsingRelationships(Contact::class, ['tags'])] + public function many_to_many_inverse(): void + { + $contact = static::contactFactory()->create([ + 'tags' => static::tagFactory()::new()->many(3), + ]); + + static::contactFactory()::assert()->count(1); + static::tagFactory()::assert()->count(3); + + $this->assertNotNull($contact->id); + + foreach ($contact->getTags() as $tag) { + $this->assertTrue($contact->getTags()->contains($tag)); + $this->assertNotNull($tag->id); + } + } + + /** @test */ + #[Test] + #[DataProvider('provideCascadeRelationshipsCombinations')] + #[UsingRelationships(Contact::class, ['address'])] + public function one_to_one_owning(): void + { + $contact = static::contactFactory()->create(); + + static::contactFactory()::assert()->count(1); + static::addressFactory()::assert()->count(1); + + $this->assertNotNull($contact->id); + $this->assertNotNull($contact->getAddress()->id); + } + + /** @test */ + #[Test] + #[DataProvider('provideCascadeRelationshipsCombinations')] + #[UsingRelationships(Address::class, ['contact'])] + #[UsingRelationships(Contact::class, ['address', 'category'])] + public function inversed_one_to_one(): void + { + $address = static::addressFactory()->create(['contact' => static::contactFactory()]); + + self::assertNotNull($address->getContact()); + + static::addressFactory()::assert()->count(1); + static::contactFactory()::assert()->count(1); + } + + /** @test */ + #[Test] + #[DataProvider('provideCascadeRelationshipsCombinations')] + #[UsingRelationships(Contact::class, ['address'])] + public function many_to_one_unmanaged_raw_entity(): void + { + $address = unproxy(static::addressFactory()->create(['city' => 'Some city'])); + + /** @var EntityManagerInterface $em */ + $em = self::getContainer()->get(EntityManagerInterface::class); + $em->clear(); + + $contact = static::contactFactory()->create(['address' => $address]); + + $this->assertSame('Some city', $contact->getAddress()->getCity()); + } + + /** @test */ + #[Test] + #[DataProvider('provideCascadeRelationshipsCombinations')] + #[UsingRelationships(Category::class, ['contacts', 'secondaryContacts'])] + public function one_to_many_with_two_relationships_same_entity(): void + { + $category = static::categoryFactory()->create([ + 'contacts' => static::contactFactory()->many(2), + + // ensure no "main category" is set for secondary contacts + 'secondaryContacts' => static::contactFactoryWithoutCategory()->many(3), + ]); + + $this->assertCount(2, $category->getContacts()); + $this->assertCount(3, $category->getSecondaryContacts()); + + static::contactFactory()::assert()->count(5); + static::categoryFactory()::assert()->count(1); + + foreach ($category->getContacts() as $contact) { + self::assertSame(unproxy($category), $contact->getCategory()); + } + + foreach ($category->getSecondaryContacts() as $contact) { + self::assertSame(unproxy($category), $contact->getSecondaryCategory()); + } + } + + /** @test */ + #[Test] + #[DataProvider('provideCascadeRelationshipsCombinations')] + #[UsingRelationships(Category::class, ['contacts', 'secondaryContacts'])] + public function one_to_many_with_two_relationships_same_entity_and_adders(): void + { + $category = static::categoryFactory()->create([ + 'addContact' => static::contactFactoryWithoutCategory(), + 'addSecondaryContact' => static::contactFactoryWithoutCategory(), + ]); + + $this->assertCount(1, $category->getContacts()); + $this->assertCount(1, $category->getSecondaryContacts()); + + static::contactFactory()::assert()->count(2); + static::categoryFactory()::assert()->count(1); + } + + /** @test */ + #[Test] + #[DataProvider('provideCascadeRelationshipsCombinations')] + #[UsingRelationships(Category::class, ['contacts', 'secondaryContacts'])] + public function inverse_many_to_many_with_two_relationships_same_entity(): void + { + static::tagFactory()::assert()->count(0); + + $tag = static::tagFactory()->create([ + 'contacts' => static::contactFactory()->many(3), + 'secondaryContacts' => static::contactFactory()->many(2), + ]); + + $this->assertCount(3, $tag->getContacts()); + $this->assertCount(2, $tag->getSecondaryContacts()); + + static::contactFactory()::assert()->count(5); + static::tagFactory()::assert()->count(1); + } + + /** @test */ + #[Test] + #[DataProvider('provideCascadeRelationshipsCombinations')] + #[UsingRelationships(Category::class, ['contacts', 'secondaryContacts'])] + public function can_use_adder_as_attributes(): void + { + $category = static::categoryFactory()->create([ + 'addContact' => static::contactFactory()->with(['name' => 'foo']), + ]); + + self::assertCount(1, $category->getContacts()); + self::assertSame('foo', $category->getContacts()[0]?->getName()); + } + + /** @test */ + #[Test] + #[DataProvider('provideCascadeRelationshipsCombinations')] + #[UsingRelationships(Category::class, ['contacts'])] + public function forced_one_to_many_with_doctrine_collection_type(): void + { + $category = static::categoryFactory() + ->instantiateWith(Instantiator::withConstructor()->alwaysForce()) + ->create([ + 'contacts' => static::contactFactory()->many(2), + ]) + ; + + self::assertCount(2, $category->getContacts()); + foreach ($category->getContacts() as $contact) { + self::assertSame(unproxy($category), $contact->getCategory()); + } + static::contactFactory()::assert()->count(2); + static::categoryFactory()::assert()->count(1); + } + + /** @test */ + #[Test] + #[DataProvider('provideCascadeRelationshipsCombinations')] + #[UsingRelationships(Contact::class, ['tags', 'category'])] + public function disabling_persistence_cascades_to_children(): void + { + $contact = static::contactFactory()->withoutPersisting()->create([ + 'tags' => static::tagFactory()::new()->many(3), + 'category' => static::categoryFactory(), + ]); + + static::contactFactory()::assert()->empty(); + static::categoryFactory()::assert()->empty(); + static::tagFactory()::assert()->empty(); + static::addressFactory()::assert()->empty(); + + $this->assertNull($contact->id); + $this->assertNull($contact->getCategory()?->id); + $this->assertNull($contact->getAddress()->id); + $this->assertCount(3, $contact->getTags()); + + foreach ($contact->getTags() as $tag) { + $this->assertNull($tag->id); + } + + $category = static::categoryFactory()->withoutPersisting()->create([ + 'contacts' => static::contactFactory()->many(3), + ]); + + static::contactFactory()::assert()->empty(); + static::categoryFactory()::assert()->empty(); + + $this->assertNull($category->id); + $this->assertCount(3, $category->getContacts()); + + foreach ($category->getContacts() as $contact) { + $this->assertSame($category->getName(), $contact->getCategory()?->getName()); + } + } + + /** @test */ + #[Test] + #[DataProvider('provideCascadeRelationshipsCombinations')] + #[UsingRelationships(Category::class, ['contacts'])] + #[UsingRelationships(Contact::class, ['tags', 'address'])] + public function ensure_one_to_many_relations_are_not_pre_persisted(): void + { + $category = static::categoryFactory() + ->afterInstantiate(function() { + static::contactFactory()::repository()->assert()->empty(); + static::addressFactory()::repository()->assert()->empty(); + static::tagFactory()::repository()->assert()->empty(); + }) + ->create([ + 'contacts' => static::contactFactory()->many(3), + ]) + ; + + $this->assertCount(3, $category->getContacts()); + + foreach ($category->getContacts() as $contact) { + $this->assertNotNull($contact->id); + } + } + + /** + * @test + */ + public function assert_updates_are_implicitly_persisted(): void + { + $category = static::categoryFactory()->create(); + $address = static::addressFactory()->create(); + + $category->setName('new name'); + + static::contactFactory()->create(['category' => $category, 'address' => $address]); + + refresh($category); + self::assertSame('new name', $category->getName()); + } + + /** @test */ + #[Test] + #[DataProvider('provideCascadeRelationshipsCombinations')] + #[UsingRelationships(Category::class, ['contacts'])] + public function it_can_add_managed_entity_to_many_to_one(): void + { + $this->it_can_add_entity_to_many_to_one( + static::categoryFactory()->create() + ); + } + + /** @test */ + #[Test] + #[DataProvider('provideCascadeRelationshipsCombinations')] + #[UsingRelationships(Category::class, ['contacts'])] + public function it_can_add_unmanaged_entity_to_many_to_one(): void + { + $this->it_can_add_entity_to_many_to_one( + static::categoryFactory()->withoutPersisting()->create() + ); + } + + /** @return PersistentObjectFactory */ + protected static function contactFactoryWithoutCategory(): PersistentObjectFactory + { + return static::contactFactory()->with(['category' => null]); + } + + /** @return PersistentObjectFactory */ + abstract protected static function contactFactory(): PersistentObjectFactory; + + /** @return PersistentObjectFactory */ + abstract protected static function categoryFactory(): PersistentObjectFactory; + + /** @return PersistentObjectFactory */ + abstract protected static function tagFactory(): PersistentObjectFactory; + + /** @return PersistentObjectFactory
*/ + abstract protected static function addressFactory(): PersistentObjectFactory; + + private function it_can_add_entity_to_many_to_one(Category $category): void + { + self::assertCount(0, $category->getContacts()); + + $contact1 = static::contactFactory()->create(['category' => $category]); + $contact2 = static::contactFactory()->create(['category' => $category]); + + static::categoryFactory()::assert()->count(1); + + self::assertCount(2, $category->getContacts()); + + self::assertSame(unproxy($category), $contact1->getCategory()); + self::assertSame(unproxy($category), $contact2->getCategory()); + } + + /** + * @param FactoryCollection>|list>|list $contacts + */ + private function one_to_many(FactoryCollection|array $contacts): void + { + $category = static::categoryFactory()->create([ + 'contacts' => $contacts, + ]); + + static::contactFactory()::assert()->count(2); + static::categoryFactory()::assert()->count(1); + + $this->assertNotNull($category->id); + $this->assertCount(2, $category->getContacts()); + + foreach ($category->getContacts() as $contact) { + $this->assertSame($category->id, $contact->getCategory()?->id); + } + } + + /** + * @param FactoryCollection>|list>|list $contacts + */ + private function many_to_many(FactoryCollection|array $contacts): void + { + $tag = static::tagFactory()->create([ + 'contacts' => $contacts, + ]); + + static::contactFactory()::assert()->count(3); + static::tagFactory()::repository()->assert()->count(1); + $this->assertNotNull($tag->id); + + foreach ($tag->getContacts() as $contact) { + $this->assertSame($tag->id, $contact->getTags()[0]?->id); + } + } +} diff --git a/tests/Integration/ORM/EntityRelationship/PolymorphicEntityFactoryRelationshipTest.php b/tests/Integration/ORM/EntityRelationship/PolymorphicEntityFactoryRelationshipTest.php new file mode 100644 index 000000000..7b85207fd --- /dev/null +++ b/tests/Integration/ORM/EntityRelationship/PolymorphicEntityFactoryRelationshipTest.php @@ -0,0 +1,49 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Tests\Integration\ORM\EntityRelationship; + +use PHPUnit\Framework\Attributes\RequiresPhpunit; +use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\Address\AddressFactory; +use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\Category\CategoryFactory; +use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\Contact\ChildContactFactory; +use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\Tag\TagFactory; + +/** + * tests behavior with inheritance. + * + * @author Kevin Bond + * @author Nicolas PHILIPPE + * @requires PHPUnit >=11.4 + */ +#[RequiresPhpunit('>=11.4')] +final class PolymorphicEntityFactoryRelationshipTest extends EntityFactoryRelationshipTestCase +{ + protected static function contactFactory(): ChildContactFactory + { + return ChildContactFactory::new(); + } + + protected static function categoryFactory(): CategoryFactory + { + return CategoryFactory::new(); + } + + protected static function tagFactory(): TagFactory + { + return TagFactory::new(); + } + + protected static function addressFactory(): AddressFactory + { + return AddressFactory::new(); + } +} diff --git a/tests/Integration/ORM/EntityRelationship/ProxyEntityFactoryRelationshipTest.php b/tests/Integration/ORM/EntityRelationship/ProxyEntityFactoryRelationshipTest.php new file mode 100644 index 000000000..ebdaca361 --- /dev/null +++ b/tests/Integration/ORM/EntityRelationship/ProxyEntityFactoryRelationshipTest.php @@ -0,0 +1,156 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Tests\Integration\ORM\EntityRelationship; + +use Doctrine\ORM\EntityManagerInterface; +use Doctrine\Persistence\Proxy as DoctrineProxy; +use PHPUnit\Framework\AssertionFailedError; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\RequiresPhpunit; +use PHPUnit\Framework\Attributes\Test; +use Zenstruck\Assert; +use Zenstruck\Foundry\Persistence\Proxy; +use Zenstruck\Foundry\Tests\Fixture\DoctrineCascadeRelationship\UsingRelationships; +use Zenstruck\Foundry\Tests\Fixture\Entity\Contact; +use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\Address\ProxyAddressFactory; +use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\Category\ProxyCategoryFactory; +use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\Contact\ProxyContactFactory; +use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\Tag\ProxyTagFactory; + +/** + * @author Kevin Bond + * @author Nicolas PHILIPPE + * @requires PHPUnit >=11.4 + */ +#[RequiresPhpunit('>=11.4')] +final class ProxyEntityFactoryRelationshipTest extends EntityFactoryRelationshipTestCase +{ + /** @test */ + #[Test] + #[DataProvider('provideCascadeRelationshipsCombinations')] + #[UsingRelationships(Contact::class, ['category'])] + public function doctrine_proxies_are_converted_to_foundry_proxies(): void + { + static::contactFactory()->create(['category' => static::categoryFactory()]); + + // clear the em so nothing is tracked + self::getContainer()->get(EntityManagerInterface::class)->clear(); // @phpstan-ignore method.notFound + + // load a random Contact which causes the em to track a "doctrine proxy" for category + static::contactFactory()::random(); + + // load a random Category which should be a "doctrine proxy" + $category = static::categoryFactory()::random(); + + // ensure the category is a "doctrine proxy" and a Category + $this->assertInstanceOf(Proxy::class, $category); + $this->assertInstanceOf(DoctrineProxy::class, $category->_real()); + $this->assertInstanceOf(static::categoryFactory()::class(), $category); + } + + /** @test */ + #[Test] + #[DataProvider('provideCascadeRelationshipsCombinations')] + #[UsingRelationships(Contact::class, ['category'])] + public function it_can_add_proxy_to_many_to_one(): void + { + $contact = static::contactFactory()->create(); + + $contact->setCategory($category = static::categoryFactory()->create()); + $contact->_save(); + + static::contactFactory()::assert()->count(1); + static::contactFactory()::assert()->exists(['category' => $category]); + } + + /** @test */ + #[Test] + #[DataProvider('provideCascadeRelationshipsCombinations')] + #[UsingRelationships(Contact::class, ['tags'])] + public function it_can_add_proxy_to_one_to_many(): void + { + $contact = static::contactFactory()->create(); + + $contact->addTag(static::tagFactory()->create()); + $contact->_save(); + + static::contactFactory()::assert()->count(1); + $tag = static::tagFactory()::first(); + self::assertContains($contact->_real(), $tag->getContacts()); + } + + /** @test */ + #[Test] + public function can_assert_persisted(): void + { + static::contactFactory()->create()->_assertPersisted(); + + Assert::that(function(): void { static::contactFactory()->withoutPersisting()->create()->_assertPersisted(); }) + ->throws(AssertionFailedError::class, \sprintf('%s is not persisted.', static::contactFactory()::class())) + ; + } + + /** @test */ + #[Test] + public function can_assert_not_persisted(): void + { + static::contactFactory()->withoutPersisting()->create()->_assertNotPersisted(); + + Assert::that(function(): void { static::contactFactory()->create()->_assertNotPersisted(); }) + ->throws(AssertionFailedError::class, \sprintf('%s is persisted but it should not be.', static::contactFactory()::class())) + ; + } + + /** @test */ + #[Test] + public function can_remove_and_assert_not_persisted(): void + { + static::contactFactory() + ->create() + ->_assertPersisted() + ->_delete() + ->_assertNotPersisted() + ; + } + + /** @test */ + #[Test] + public function can_use_assert_persisted_when_entity_has_changes(): void + { + $contact = static::contactFactory()->create(); + $contact->setName('foo'); + + $contact->_assertPersisted(); + } + + protected static function contactFactory(): ProxyContactFactory + { + return ProxyContactFactory::new(); + } + + protected static function categoryFactory(): ProxyCategoryFactory + { + return ProxyCategoryFactory::new(); + } + + protected static function tagFactory(): ProxyTagFactory + { + return ProxyTagFactory::new(); + } + + protected static function addressFactory(): ProxyAddressFactory + { + return ProxyAddressFactory::new(); + } +} diff --git a/tests/Integration/ORM/EntityRelationship/StandardEntityFactoryRelationshipTest.php b/tests/Integration/ORM/EntityRelationship/StandardEntityFactoryRelationshipTest.php new file mode 100644 index 000000000..f9b217cf8 --- /dev/null +++ b/tests/Integration/ORM/EntityRelationship/StandardEntityFactoryRelationshipTest.php @@ -0,0 +1,49 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Tests\Integration\ORM\EntityRelationship; + +use PHPUnit\Framework\Attributes\RequiresPhpunit; +use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\Address\AddressFactory; +use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\Category\CategoryFactory; +use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\Contact\ContactFactory; +use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\Tag\TagFactory; + +/** + * @author Kevin Bond + * @author Nicolas PHILIPPE + * @requires PHPUnit >=11.4 + */ +#[RequiresPhpunit('>=11.4')] +final class StandardEntityFactoryRelationshipTest extends EntityFactoryRelationshipTestCase +{ + protected static function contactFactory(): ContactFactory + { + return ContactFactory::new(); + } + + protected static function categoryFactory(): CategoryFactory + { + return CategoryFactory::new(); + } + + protected static function tagFactory(): TagFactory + { + return TagFactory::new(); + } + + protected static function addressFactory(): AddressFactory + { + return AddressFactory::new(); + } +} diff --git a/tests/Integration/ORM/GenericEntityFactoryTest.php b/tests/Integration/ORM/GenericEntityFactoryTest.php index 9b7458758..198e279ba 100644 --- a/tests/Integration/ORM/GenericEntityFactoryTest.php +++ b/tests/Integration/ORM/GenericEntityFactoryTest.php @@ -12,7 +12,6 @@ namespace Zenstruck\Foundry\Tests\Integration\ORM; use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\GenericEntityFactory; -use Zenstruck\Foundry\Tests\Fixture\Factories\GenericModelFactory; use Zenstruck\Foundry\Tests\Integration\Persistence\GenericFactoryTestCase; use Zenstruck\Foundry\Tests\Integration\RequiresORM; @@ -23,7 +22,7 @@ final class GenericEntityFactoryTest extends GenericFactoryTestCase { use RequiresORM; - protected function factory(): GenericModelFactory + protected static function factory(): GenericEntityFactory { return GenericEntityFactory::new(); } diff --git a/tests/Integration/ORM/GenericEntityProxyFactoryTest.php b/tests/Integration/ORM/GenericEntityProxyFactoryTest.php index 7cf8a82c9..8d0f47a39 100644 --- a/tests/Integration/ORM/GenericEntityProxyFactoryTest.php +++ b/tests/Integration/ORM/GenericEntityProxyFactoryTest.php @@ -26,7 +26,7 @@ final class GenericEntityProxyFactoryTest extends GenericProxyFactoryTestCase { use RequiresORM; - protected function factory(): PersistentProxyObjectFactory + protected static function factory(): GenericProxyEntityFactory { return GenericProxyEntityFactory::new(); } diff --git a/tests/Integration/ORM/PersistenceManagerTest.php b/tests/Integration/ORM/PersistenceManagerTest.php new file mode 100644 index 000000000..26258e356 --- /dev/null +++ b/tests/Integration/ORM/PersistenceManagerTest.php @@ -0,0 +1,35 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Tests\Integration\ORM; + +use Doctrine\ORM\EntityManagerInterface; +use Doctrine\Persistence\ObjectManager; +use Zenstruck\Foundry\Tests\Fixture\Entity\EntityWithUid; +use Zenstruck\Foundry\Tests\Integration\Persistence\PersistenceManagerTestCase; +use Zenstruck\Foundry\Tests\Integration\RequiresORM; + +final class PersistenceManagerTest extends PersistenceManagerTestCase +{ + use RequiresORM; + + protected static function createObject(): object + { + return new EntityWithUid(); + } + + protected static function objectManager(): ObjectManager + { + return self::getContainer()->get(EntityManagerInterface::class); // @phpstan-ignore return.type + } +} diff --git a/tests/Integration/ORM/PolymorphicEntityFactoryRelationshipTest.php b/tests/Integration/ORM/PolymorphicEntityFactoryRelationshipTest.php deleted file mode 100644 index 92607a6b7..000000000 --- a/tests/Integration/ORM/PolymorphicEntityFactoryRelationshipTest.php +++ /dev/null @@ -1,26 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Zenstruck\Foundry\Tests\Integration\ORM; - -use Zenstruck\Foundry\Persistence\PersistentObjectFactory; -use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\Contact\ChildContactFactory; - -/** - * tests behavior with inheritance. - */ -class PolymorphicEntityFactoryRelationshipTest extends StandardEntityFactoryRelationshipTest -{ - protected function contactFactory(): PersistentObjectFactory - { - return ChildContactFactory::new(); // @phpstan-ignore return.type - } -} diff --git a/tests/Integration/ORM/ProxyCascadeEntityFactoryRelationshipTest.php b/tests/Integration/ORM/ProxyCascadeEntityFactoryRelationshipTest.php deleted file mode 100644 index e466a2465..000000000 --- a/tests/Integration/ORM/ProxyCascadeEntityFactoryRelationshipTest.php +++ /dev/null @@ -1,44 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Zenstruck\Foundry\Tests\Integration\ORM; - -use Zenstruck\Foundry\Persistence\PersistentObjectFactory; -use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\Address\ProxyCascadeAddressFactory; -use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\Category\ProxyCascadeCategoryFactory; -use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\Contact\ProxyCascadeContactFactory; -use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\Tag\ProxyCascadeTagFactory; - -/** - * @author Nicolas PHILIPPE - */ -final class ProxyCascadeEntityFactoryRelationshipTest extends ProxyEntityFactoryRelationshipTestCase -{ - protected function contactFactory(): PersistentObjectFactory - { - return ProxyCascadeContactFactory::new(); // @phpstan-ignore return.type - } - - protected function categoryFactory(): PersistentObjectFactory - { - return ProxyCascadeCategoryFactory::new(); // @phpstan-ignore return.type - } - - protected function tagFactory(): PersistentObjectFactory - { - return ProxyCascadeTagFactory::new(); // @phpstan-ignore return.type - } - - protected function addressFactory(): PersistentObjectFactory - { - return ProxyCascadeAddressFactory::new(); // @phpstan-ignore return.type - } -} diff --git a/tests/Integration/ORM/ProxyEntityFactoryRelationshipTest.php b/tests/Integration/ORM/ProxyEntityFactoryRelationshipTest.php deleted file mode 100644 index a56852831..000000000 --- a/tests/Integration/ORM/ProxyEntityFactoryRelationshipTest.php +++ /dev/null @@ -1,44 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Zenstruck\Foundry\Tests\Integration\ORM; - -use Zenstruck\Foundry\Persistence\PersistentObjectFactory; -use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\Address\ProxyAddressFactory; -use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\Category\ProxyCategoryFactory; -use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\Contact\ProxyContactFactory; -use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\Tag\ProxyTagFactory; - -/** - * @author Kevin Bond - */ -final class ProxyEntityFactoryRelationshipTest extends ProxyEntityFactoryRelationshipTestCase -{ - protected function contactFactory(): PersistentObjectFactory - { - return ProxyContactFactory::new(); // @phpstan-ignore return.type - } - - protected function categoryFactory(): PersistentObjectFactory - { - return ProxyCategoryFactory::new(); // @phpstan-ignore return.type - } - - protected function tagFactory(): PersistentObjectFactory - { - return ProxyTagFactory::new(); // @phpstan-ignore return.type - } - - protected function addressFactory(): PersistentObjectFactory - { - return ProxyAddressFactory::new(); // @phpstan-ignore return.type - } -} diff --git a/tests/Integration/ORM/ProxyEntityFactoryRelationshipTestCase.php b/tests/Integration/ORM/ProxyEntityFactoryRelationshipTestCase.php deleted file mode 100644 index 400d777bc..000000000 --- a/tests/Integration/ORM/ProxyEntityFactoryRelationshipTestCase.php +++ /dev/null @@ -1,137 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Zenstruck\Foundry\Tests\Integration\ORM; - -use Doctrine\ORM\EntityManagerInterface; -use Doctrine\Persistence\Proxy as DoctrineProxy; -use PHPUnit\Framework\AssertionFailedError; -use Zenstruck\Assert; -use Zenstruck\Foundry\Persistence\Exception\RefreshObjectFailed; -use Zenstruck\Foundry\Persistence\PersistentProxyObjectFactory; -use Zenstruck\Foundry\Persistence\Proxy; -use Zenstruck\Foundry\Tests\Fixture\Entity\Address; -use Zenstruck\Foundry\Tests\Fixture\Entity\Category; -use Zenstruck\Foundry\Tests\Fixture\Entity\Contact; -use Zenstruck\Foundry\Tests\Fixture\Entity\Tag; - -/** - * @author Nicolas PHILIPPE - * - * @method PersistentProxyObjectFactory contactFactory() - * @method PersistentProxyObjectFactory categoryFactory() - * @method PersistentProxyObjectFactory tagFactory() - * @method PersistentProxyObjectFactory
addressFactory() - */ -abstract class ProxyEntityFactoryRelationshipTestCase extends EntityFactoryRelationshipTestCase -{ - /** - * @see https://github.com/zenstruck/foundry/issues/42 - * - * @test - */ - public function doctrine_proxies_are_converted_to_foundry_proxies(): void - { - $this->contactFactory()->create(['category' => $this->categoryFactory()]); - - // clear the em so nothing is tracked - self::getContainer()->get(EntityManagerInterface::class)->clear(); // @phpstan-ignore method.nonObject - - // load a random Contact which causes the em to track a "doctrine proxy" for category - $this->contactFactory()::random(); - - // load a random Category which should be a "doctrine proxy" - $category = $this->categoryFactory()::random(); - - // ensure the category is a "doctrine proxy" and a Category - $this->assertInstanceOf(Proxy::class, $category); - $this->assertInstanceOf(DoctrineProxy::class, $category->_real()); - $this->assertInstanceOf($this->categoryFactory()::class(), $category); - } - - /** - * @test - */ - public function it_can_add_proxy_to_many_to_one(): void - { - $contact = $this->contactFactory()->create(); - - $contact->setCategory($category = $this->categoryFactory()->create()); - $contact->_save(); - - $this->contactFactory()::assert()->count(1); - $this->contactFactory()::assert()->exists(['category' => $category]); - } - - /** - * @test - */ - public function it_can_add_proxy_to_one_to_many(): void - { - $contact = $this->contactFactory()->create(); - - $contact->addTag($this->tagFactory()->create()); - $contact->_save(); - - $this->contactFactory()::assert()->count(1); - $tag = $this->tagFactory()::first(); - self::assertContains($contact->_real(), $tag->getContacts()); - } - - /** - * @test - */ - public function can_assert_persisted(): void - { - $this->contactFactory()->create()->_assertPersisted(); - - Assert::that(function(): void { $this->contactFactory()->withoutPersisting()->create()->_assertPersisted(); }) - ->throws(AssertionFailedError::class, \sprintf('%s is not persisted.', $this->contactFactory()::class())) - ; - } - - /** - * @test - */ - public function can_assert_not_persisted(): void - { - $this->contactFactory()->withoutPersisting()->create()->_assertNotPersisted(); - - Assert::that(function(): void { $this->contactFactory()->create()->_assertNotPersisted(); }) - ->throws(AssertionFailedError::class, \sprintf('%s is persisted but it should not be.', $this->contactFactory()::class())) - ; - } - - /** - * @test - */ - public function can_remove_and_assert_not_persisted(): void - { - $this->contactFactory() - ->create() - ->_assertPersisted() - ->_delete() - ->_assertNotPersisted() - ; - } - - /** - * @test - */ - public function cannot_use_assert_persisted_when_entity_has_changes(): void - { - $contact = $this->contactFactory()->create(); - $contact->setName('foo'); - - $this->expectException(RefreshObjectFailed::class); - $contact->_assertPersisted(); - } -} diff --git a/tests/Integration/ORM/ProxyGenericEntityRepositoryDecoratorTest.php b/tests/Integration/ORM/ProxyGenericEntityRepositoryDecoratorTest.php index 844ea31a7..6fb2f5402 100644 --- a/tests/Integration/ORM/ProxyGenericEntityRepositoryDecoratorTest.php +++ b/tests/Integration/ORM/ProxyGenericEntityRepositoryDecoratorTest.php @@ -11,7 +11,7 @@ * file that was distributed with this source code. */ -namespace Integration\ORM; +namespace Zenstruck\Foundry\Tests\Integration\ORM; use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\GenericProxyEntityFactory; use Zenstruck\Foundry\Tests\Integration\Persistence\GenericRepositoryDecoratorTestCase; diff --git a/tests/Integration/ORM/StandardEntityFactoryRelationshipTest.php b/tests/Integration/ORM/StandardEntityFactoryRelationshipTest.php deleted file mode 100644 index 5189be1cf..000000000 --- a/tests/Integration/ORM/StandardEntityFactoryRelationshipTest.php +++ /dev/null @@ -1,44 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Zenstruck\Foundry\Tests\Integration\ORM; - -use Zenstruck\Foundry\Persistence\PersistentObjectFactory; -use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\Address\StandardAddressFactory; -use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\Category\StandardCategoryFactory; -use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\Contact\StandardContactFactory; -use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\Tag\StandardTagFactory; - -/** - * @author Nicolas PHILIPPE - */ -class StandardEntityFactoryRelationshipTest extends EntityFactoryRelationshipTestCase -{ - protected function contactFactory(): PersistentObjectFactory - { - return StandardContactFactory::new(); // @phpstan-ignore return.type - } - - protected function categoryFactory(): PersistentObjectFactory - { - return StandardCategoryFactory::new(); // @phpstan-ignore return.type - } - - protected function tagFactory(): PersistentObjectFactory - { - return StandardTagFactory::new(); // @phpstan-ignore return.type - } - - protected function addressFactory(): PersistentObjectFactory - { - return StandardAddressFactory::new(); // @phpstan-ignore return.type - } -} diff --git a/tests/Integration/ObjectFactoryTest.php b/tests/Integration/ObjectFactoryTest.php index 4dbaf095b..96f3ca87e 100644 --- a/tests/Integration/ObjectFactoryTest.php +++ b/tests/Integration/ObjectFactoryTest.php @@ -44,4 +44,20 @@ public function can_create_non_service_factories(): void $this->assertSame('router-constructor', $object->object->getProp1()); } + + /** + * @test + */ + public function can_create_different_objects_based_on_same_factory(): void + { + $factory = Object1Factory::new(['prop1' => 'first object']); + $object1 = $factory->create(); + self::assertSame('first object-constructor', $object1->getProp1()); + + $object2 = $factory->create(['prop1' => 'second object']); + self::assertSame('second object-constructor', $object2->getProp1()); + + $object3 = $factory->with(['prop1' => 'third object'])->create(); + self::assertSame('third object-constructor', $object3->getProp1()); + } } diff --git a/tests/Integration/Persistence/FactoryWithHooksInInitializeTest.php b/tests/Integration/Persistence/FactoryWithHooksInInitializeTest.php new file mode 100644 index 000000000..4cb182a76 --- /dev/null +++ b/tests/Integration/Persistence/FactoryWithHooksInInitializeTest.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Tests\Integration\Persistence; + +use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; +use Zenstruck\Foundry\Test\Factories; +use Zenstruck\Foundry\Tests\Fixture\Factories\WithHooksInInitializeFactory; + +/** + * @author Nicolas Philippe + */ +final class FactoryWithHooksInInitializeTest extends KernelTestCase +{ + use Factories; + + /** + * @test + */ + public function it_can_access_current_factory_in_hooks(): void + { + $address = WithHooksInInitializeFactory::new()->withoutPersisting()->create(); + + self::assertSame('beforeInstantiate - afterInstantiate', $address->getCity()); + } +} diff --git a/tests/Integration/Persistence/GenericFactoryTestCase.php b/tests/Integration/Persistence/GenericFactoryTestCase.php index af504f9cd..de4b4c2ef 100644 --- a/tests/Integration/Persistence/GenericFactoryTestCase.php +++ b/tests/Integration/Persistence/GenericFactoryTestCase.php @@ -43,27 +43,27 @@ abstract class GenericFactoryTestCase extends KernelTestCase */ public function can_create_and_update(): void { - $this->factory()::assert()->empty(); + static::factory()::assert()->empty(); - $object = $this->factory()->create(); + $object = static::factory()->create(); $this->assertNotNull($object->id); $this->assertSame('default1', $object->getProp1()); - $this->factory()::assert() + static::factory()::assert() ->count(1) ->exists(['prop1' => 'default1']) ->notExists(['prop1' => 'invalid']) ; - $this->assertSame($object->id, $this->factory()->first()->id); - $this->assertSame($object->id, $this->factory()->last()->id); + $this->assertSame($object->id, static::factory()->first()->id); + $this->assertSame($object->id, static::factory()->last()->id); $object->setProp1('new value'); save($object); $this->assertSame('new value', $object->getProp1()); - $this->factory()::assert()->exists(['prop1' => 'new value']); + static::factory()::assert()->exists(['prop1' => 'new value']); } /** @@ -71,18 +71,18 @@ public function can_create_and_update(): void */ public function can_disable_auto_persist(): void { - $this->factory()->repository()->assert()->empty(); + static::factory()->repository()->assert()->empty(); - $object = $this->factory()->withoutPersisting()->create(); + $object = static::factory()->withoutPersisting()->create(); $this->assertNull($object->id); $this->assertSame('default1', $object->getProp1()); - $this->factory()->repository()->assert()->empty(); + static::factory()->repository()->assert()->empty(); save($object); - $this->factory()->repository()->assert()->exists(['prop1' => 'default1']); + static::factory()->repository()->assert()->exists(['prop1' => 'default1']); } /** @@ -90,16 +90,16 @@ public function can_disable_auto_persist(): void */ public function can_refresh(): void { - $object = $this->factory()->create(); + $object = static::factory()->create(); // initial data $this->assertSame('default1', $object->getProp1()); - $this->factory()->repository()->assert()->exists(['prop1' => 'default1']); + static::factory()->repository()->assert()->exists(['prop1' => 'default1']); self::ensureKernelShutdown(); // modify and save title "externally" - $ext = $this->factory()->first(); + $ext = static::factory()->first(); $ext->setProp1('external'); save($ext); @@ -109,7 +109,7 @@ public function can_refresh(): void $this->assertSame($refreshed, $object); $this->assertSame('external', $object->getProp1()); - $this->factory()->repository()->assert()->exists(['prop1' => 'external']); + static::factory()->repository()->assert()->exists(['prop1' => 'external']); } /** @@ -117,18 +117,18 @@ public function can_refresh(): void */ public function cannot_refresh_if_there_are_unsaved_changes(): void { - $object = $this->factory()->create(); + $object = static::factory()->create(); // initial data $this->assertSame('default1', $object->getProp1()); - $this->factory()->repository()->assert()->exists(['prop1' => 'default1']); + static::factory()->repository()->assert()->exists(['prop1' => 'default1']); $object->setProp1('new'); try { refresh($object); } catch (\RuntimeException) { - $this->factory()->repository()->assert()->exists(['prop1' => 'default1']); + static::factory()->repository()->assert()->exists(['prop1' => 'default1']); return; } @@ -141,13 +141,13 @@ public function cannot_refresh_if_there_are_unsaved_changes(): void */ public function can_delete(): void { - $object = $this->factory()->create(); + $object = static::factory()->create(); - $this->factory()->repository()->assert()->exists(['prop1' => 'default1']); + static::factory()->repository()->assert()->exists(['prop1' => 'default1']); delete($object); - $this->factory()->repository()->assert()->empty(); + static::factory()->repository()->assert()->empty(); } /** @@ -170,9 +170,9 @@ public function repository_and_create_function(): void */ public function create_many(): void { - $models = $this->factory()->createMany(3, fn(int $i) => ['prop1' => "value{$i}"]); + $models = static::factory()->createMany(3, fn(int $i) => ['prop1' => "value{$i}"]); - $this->factory()::repository()->assert()->count(3); + static::factory()::repository()->assert()->count(3); $this->assertSame('value1', $models[0]->getProp1()); $this->assertSame('value2', $models[1]->getProp1()); @@ -184,10 +184,10 @@ public function create_many(): void */ public function find(): void { - $object = $this->factory()->create(['prop1' => 'foo']); + $object = static::factory()->create(['prop1' => 'foo']); - $this->assertSame($object->id, $this->factory()::find($object->id)->id); - $this->assertSame($object->id, $this->factory()::find(['prop1' => 'foo'])->id); + $this->assertSame($object->id, static::factory()::find($object->id)->id); + $this->assertSame($object->id, static::factory()::find(['prop1' => 'foo'])->id); } /** @@ -197,7 +197,7 @@ public function find_must_return_object(): void { $this->expectException(\RuntimeException::class); - $this->factory()::find(1); + static::factory()::find(1); } /** @@ -205,12 +205,12 @@ public function find_must_return_object(): void */ public function find_by(): void { - $this->factory()->create(['prop1' => 'a']); - $this->factory()->create(['prop1' => 'b']); - $this->factory()->create(['prop1' => 'b']); + static::factory()->create(['prop1' => 'a']); + static::factory()->create(['prop1' => 'b']); + static::factory()->create(['prop1' => 'b']); - $this->assertCount(1, $this->factory()::findBy(['prop1' => 'a'])); - $this->assertCount(2, $this->factory()::findBy(['prop1' => 'b'])); + $this->assertCount(1, static::factory()::findBy(['prop1' => 'a'])); + $this->assertCount(2, static::factory()::findBy(['prop1' => 'b'])); } /** @@ -218,15 +218,15 @@ public function find_by(): void */ public function find_or_create(): void { - $this->factory()->create(['prop1' => 'a']); + static::factory()->create(['prop1' => 'a']); - $this->assertSame('a', $this->factory()::findOrCreate(['prop1' => 'a'])->getProp1()); + $this->assertSame('a', static::factory()::findOrCreate(['prop1' => 'a'])->getProp1()); - $this->factory()::repository()->assert()->count(1); + static::factory()::repository()->assert()->count(1); - $this->assertSame('b', $this->factory()::findOrCreate(['prop1' => 'b'])->getProp1()); + $this->assertSame('b', static::factory()::findOrCreate(['prop1' => 'b'])->getProp1()); - $this->factory()::repository()->assert()->count(2); + static::factory()::repository()->assert()->count(2); } /** @@ -234,12 +234,12 @@ public function find_or_create(): void */ public function random(): void { - $this->factory()->create(['prop1' => 'a']); - $this->factory()->create(['prop1' => 'b']); - $this->factory()->create(['prop1' => 'c']); + static::factory()->create(['prop1' => 'a']); + static::factory()->create(['prop1' => 'b']); + static::factory()->create(['prop1' => 'c']); - $this->assertContains($this->factory()::random()->getProp1(), ['a', 'b', 'c']); - $this->assertSame('b', $this->factory()::random(['prop1' => 'b'])->getProp1()); + $this->assertContains(static::factory()::random()->getProp1(), ['a', 'b', 'c']); + $this->assertSame('b', static::factory()::random(['prop1' => 'b'])->getProp1()); } /** @@ -249,7 +249,7 @@ public function random_must_return_an_object(): void { $this->expectException(NotEnoughObjects::class); - $this->factory()::random(); + static::factory()::random(); } /** @@ -257,16 +257,16 @@ public function random_must_return_an_object(): void */ public function random_or_create(): void { - $this->factory()->create(['prop1' => 'a']); + static::factory()->create(['prop1' => 'a']); - $this->assertSame('a', $this->factory()::randomOrCreate()->getProp1()); - $this->assertSame('a', $this->factory()::randomOrCreate(['prop1' => 'a'])->getProp1()); + $this->assertSame('a', static::factory()::randomOrCreate()->getProp1()); + $this->assertSame('a', static::factory()::randomOrCreate(['prop1' => 'a'])->getProp1()); - $this->factory()::repository()->assert()->count(1); + static::factory()::repository()->assert()->count(1); - $this->assertSame('b', $this->factory()::randomOrCreate(['prop1' => 'b'])->getProp1()); + $this->assertSame('b', static::factory()::randomOrCreate(['prop1' => 'b'])->getProp1()); - $this->factory()::repository()->assert()->count(2); + static::factory()::repository()->assert()->count(2); } /** @@ -274,17 +274,17 @@ public function random_or_create(): void */ public function random_set(): void { - $this->factory()->create(['prop1' => 'a']); - $this->factory()->create(['prop1' => 'b']); - $this->factory()->create(['prop1' => 'b']); + static::factory()->create(['prop1' => 'a']); + static::factory()->create(['prop1' => 'b']); + static::factory()->create(['prop1' => 'b']); - $set = $this->factory()::randomSet(2); + $set = static::factory()::randomSet(2); $this->assertCount(2, $set); $this->assertContains($set[0]->getProp1(), ['a', 'b']); $this->assertContains($set[1]->getProp1(), ['a', 'b']); - $set = $this->factory()::randomSet(2, ['prop1' => 'b']); + $set = static::factory()::randomSet(2, ['prop1' => 'b']); $this->assertCount(2, $set); $this->assertSame('b', $set[0]->getProp1()); @@ -296,11 +296,11 @@ public function random_set(): void */ public function random_set_requires_at_least_the_number_available(): void { - $this->factory()::createMany(3); + static::factory()::createMany(3); $this->expectException(NotEnoughObjects::class); - $this->factory()::randomSet(4); + static::factory()::randomSet(4); } /** @@ -308,12 +308,12 @@ public function random_set_requires_at_least_the_number_available(): void */ public function random_range(): void { - $this->factory()->create(['prop1' => 'a']); - $this->factory()->create(['prop1' => 'b']); - $this->factory()->create(['prop1' => 'b']); - $this->factory()->create(['prop1' => 'b']); + static::factory()->create(['prop1' => 'a']); + static::factory()->create(['prop1' => 'b']); + static::factory()->create(['prop1' => 'b']); + static::factory()->create(['prop1' => 'b']); - $range = $this->factory()::randomRange(0, 3); + $range = static::factory()::randomRange(0, 3); $this->assertGreaterThanOrEqual(0, \count($this)); $this->assertLessThanOrEqual(3, \count($this)); @@ -322,7 +322,7 @@ public function random_range(): void $this->assertContains($object->getProp1(), ['a', 'b']); } - $range = $this->factory()::randomRange(0, 3, ['prop1' => 'b']); + $range = static::factory()::randomRange(0, 3, ['prop1' => 'b']); $this->assertGreaterThanOrEqual(0, \count($this)); $this->assertLessThanOrEqual(3, \count($this)); @@ -337,11 +337,11 @@ public function random_range(): void */ public function random_range_requires_at_least_the_max_available(): void { - $this->factory()::createMany(3); + static::factory()::createMany(3); $this->expectException(NotEnoughObjects::class); - $this->factory()::randomRange(1, 5); + static::factory()::randomRange(1, 5); } /** @@ -349,12 +349,12 @@ public function random_range_requires_at_least_the_max_available(): void */ public function factory_count(): void { - $this->factory()::createOne(['prop1' => 'a']); - $this->factory()::createOne(['prop1' => 'b']); - $this->factory()::createOne(['prop1' => 'b']); + static::factory()::createOne(['prop1' => 'a']); + static::factory()::createOne(['prop1' => 'b']); + static::factory()::createOne(['prop1' => 'b']); - $this->assertSame(3, $this->factory()::count()); - $this->assertSame(2, $this->factory()::count(['prop1' => 'b'])); + $this->assertSame(3, static::factory()::count()); + $this->assertSame(2, static::factory()::count(['prop1' => 'b'])); } /** @@ -362,12 +362,12 @@ public function factory_count(): void */ public function truncate(): void { - $this->factory()::createMany(3); - $this->factory()::repository()->assert()->count(3); + static::factory()::createMany(3); + static::factory()::repository()->assert()->count(3); - $this->factory()::truncate(); + static::factory()::truncate(); - $this->factory()::repository()->assert()->empty(); + static::factory()::repository()->assert()->empty(); } /** @@ -375,9 +375,9 @@ public function truncate(): void */ public function factory_all(): void { - $this->factory()::createMany(3); + static::factory()::createMany(3); - $this->assertCount(3, $this->factory()::all()); + $this->assertCount(3, static::factory()::all()); } /** @@ -385,14 +385,14 @@ public function factory_all(): void */ public function repository_assertions(): void { - $assert = $this->factory()::repository()->assert(); + $assert = static::factory()::repository()->assert(); $assert->empty(); $assert->empty(['prop1' => 'a']); - $this->factory()::createOne(['prop1' => 'a']); - $this->factory()::createOne(['prop1' => 'b']); - $this->factory()::createOne(['prop1' => 'b']); + static::factory()::createOne(['prop1' => 'a']); + static::factory()::createOne(['prop1' => 'b']); + static::factory()::createOne(['prop1' => 'b']); $assert->notEmpty(); $assert->notEmpty(['prop1' => 'a']); @@ -415,9 +415,9 @@ public function repository_assertions(): void */ public function repository_is_lazy(): void { - $this->factory()::createOne(); + static::factory()::createOne(); - $repository = $this->factory()::repository(); + $repository = static::factory()::repository(); $object = $repository->random(); $object->setProp1('new value'); @@ -433,21 +433,21 @@ public function repository_is_lazy(): void */ public function flush_after(): void { - $this->factory()::repository()->assert()->empty(); + static::factory()::repository()->assert()->empty(); $object = null; $return = flush_after(function() use (&$object) { - $object = $this->factory()::createOne(); + $object = static::factory()::createOne(); // ensure auto-refresh does not break when in flush_after $object->getProp1(); - $this->factory()::repository()->assert()->empty(); + static::factory()::repository()->assert()->empty(); return $object; }); - $this->factory()::repository()->assert()->count(1); + static::factory()::repository()->assert()->count(1); self::assertSame($object, $return); } @@ -456,19 +456,19 @@ public function flush_after(): void */ public function can_disable_and_enable_persisting_globally(): void { - $this->factory()::repository()->assert()->empty(); + static::factory()::repository()->assert()->empty(); disable_persisting(); - $this->factory()::createOne(); - $this->factory()::new()->create(); + static::factory()::createOne(); + static::factory()::new()->create(); persistent_factory($this->modelClass())->create(['prop1' => 'foo']); persist($this->modelClass(), ['prop1' => 'foo']); enable_persisting(); - $this->factory()::createOne(); - $this->factory()::repository()->assert()->count(1); + static::factory()::createOne(); + static::factory()::repository()->assert()->count(1); } /** @@ -480,19 +480,19 @@ public function cannot_access_repository_method_when_persist_disabled(): void $countErrors = 0; try { - $this->factory()::assert(); + static::factory()::assert(); } catch (PersistenceDisabled) { ++$countErrors; } try { - $this->factory()::repository(); + static::factory()::repository(); } catch (PersistenceDisabled) { ++$countErrors; } try { - $this->factory()::findBy([]); + static::factory()::findBy([]); } catch (PersistenceDisabled) { ++$countErrors; } @@ -505,11 +505,11 @@ public function cannot_access_repository_method_when_persist_disabled(): void */ public function can_persist_object_with_sequence(): void { - $this->factory()->sequence([['prop1' => 'foo'], ['prop1' => 'bar']])->create(); + static::factory()->sequence([['prop1' => 'foo'], ['prop1' => 'bar']])->create(); - $this->factory()::assert()->count(2); - $this->factory()::assert()->exists(['prop1' => 'foo']); - $this->factory()::assert()->exists(['prop1' => 'bar']); + static::factory()::assert()->count(2); + static::factory()::assert()->exists(['prop1' => 'foo']); + static::factory()::assert()->exists(['prop1' => 'bar']); } /** @@ -523,7 +523,7 @@ public function assert_persist_is_re_enabled_automatically(): void self::assertTrue($configuration->persistence()->isEnabled()); persist($this->modelClass(), ['prop1' => 'value']); - $this->factory()::assert()->count(1); + static::factory()::assert()->count(1); } /** @@ -531,7 +531,7 @@ public function assert_persist_is_re_enabled_automatically(): void */ public function assert_it_ca_create_object_with_dates(): void { - $object = $this->factory()->create(['date' => $date = new \DateTimeImmutable()]); + $object = static::factory()->create(['date' => $date = new \DateTimeImmutable()]); self::assertSame($date->format(\DateTimeInterface::ATOM), $object->getDate()?->format(\DateTimeInterface::ATOM)); } @@ -549,11 +549,11 @@ public function it_should_not_create_proxy_for_not_persistable_objects(): void */ protected function modelClass(): string { - return $this->factory()::class(); + return static::factory()::class(); } /** * @return PersistentObjectFactory */ - abstract protected function factory(): PersistentObjectFactory; + abstract protected static function factory(): PersistentObjectFactory; } diff --git a/tests/Integration/Persistence/GenericProxyFactoryTestCase.php b/tests/Integration/Persistence/GenericProxyFactoryTestCase.php index 6728a18c4..014ed2a0f 100644 --- a/tests/Integration/Persistence/GenericProxyFactoryTestCase.php +++ b/tests/Integration/Persistence/GenericProxyFactoryTestCase.php @@ -11,6 +11,7 @@ namespace Zenstruck\Foundry\Tests\Integration\Persistence; +use PHPUnit\Framework\Attributes\Test; use Zenstruck\Foundry\Object\Instantiator; use Zenstruck\Foundry\Persistence\PersistentProxyObjectFactory; use Zenstruck\Foundry\Persistence\Proxy; @@ -18,6 +19,7 @@ use Zenstruck\Foundry\Tests\Fixture\Entity\EdgeCases\EntityWithReadonly\EntityWithReadonly; use Zenstruck\Foundry\Tests\Fixture\Model\Embeddable; use Zenstruck\Foundry\Tests\Fixture\Model\GenericModel; + use function Zenstruck\Foundry\factory; /** @@ -30,32 +32,32 @@ abstract class GenericProxyFactoryTestCase extends GenericFactoryTestCase */ public function can_update_and_delete_via_proxy(): void { - $this->factory()->repository()->assert()->empty(); + static::factory()->repository()->assert()->empty(); - $object = $this->factory()->create(); + $object = static::factory()->create(); $this->assertNotNull($object->id); $this->assertSame('default1', $object->getProp1()); $this->assertSame('default1', $object->_refresh()->getProp1()); - $this->factory()->repository()->assert() + static::factory()->repository()->assert() ->count(1) ->exists(['prop1' => 'default1']) ->notExists(['prop1' => 'invalid']) ; - $this->assertSame($object->id, $this->factory()->first()->id); - $this->assertSame($object->id, $this->factory()->last()->id); + $this->assertSame($object->id, static::factory()->first()->id); + $this->assertSame($object->id, static::factory()->last()->id); $object->setProp1('new value'); $object->_save(); $this->assertSame('new value', $object->getProp1()); - $this->factory()->repository()->assert()->exists(['prop1' => 'new value']); + static::factory()->repository()->assert()->exists(['prop1' => 'new value']); $object->_delete(); - $this->factory()->repository()->assert()->empty(); + static::factory()->repository()->assert()->empty(); } /** @@ -63,18 +65,18 @@ public function can_update_and_delete_via_proxy(): void */ public function can_disable_persisting_by_factory_and_save_proxy(): void { - $this->factory()->repository()->assert()->empty(); + static::factory()->repository()->assert()->empty(); - $object = $this->factory()->withoutPersisting()->create()->_disableAutoRefresh(); + $object = static::factory()->withoutPersisting()->create()->_disableAutoRefresh(); $this->assertNull($object->id); $this->assertSame('default1', $object->getProp1()); - $this->factory()->repository()->assert()->empty(); + static::factory()->repository()->assert()->empty(); $object->_save(); - $this->factory()->repository()->assert()->exists(['prop1' => 'default1']); + static::factory()->repository()->assert()->exists(['prop1' => 'default1']); } /** @@ -82,11 +84,11 @@ public function can_disable_persisting_by_factory_and_save_proxy(): void */ public function can_disable_and_enable_proxy_auto_refreshing(): void { - $object = $this->factory()->create(); + $object = static::factory()->create(); // initial data $this->assertSame('default1', $object->getProp1()); - $this->factory()->repository()->assert()->exists(['prop1' => 'default1']); + static::factory()->repository()->assert()->exists(['prop1' => 'default1']); $object->_disableAutoRefresh(); $object->setProp1('new'); @@ -95,7 +97,7 @@ public function can_disable_and_enable_proxy_auto_refreshing(): void $object->_save(); $this->assertSame('new 2', $object->getProp1()); - $this->factory()->repository()->assert()->exists(['prop1' => 'new 2']); + static::factory()->repository()->assert()->exists(['prop1' => 'new 2']); } /** @@ -103,11 +105,11 @@ public function can_disable_and_enable_proxy_auto_refreshing(): void */ public function can_disable_and_enable_proxy_auto_refreshing_with_callback(): void { - $object = $this->factory()->create(); + $object = static::factory()->create(); // initial data $this->assertSame('default1', $object->getProp1()); - $this->factory()->repository()->assert()->exists(['prop1' => 'default1']); + static::factory()->repository()->assert()->exists(['prop1' => 'default1']); $object->_withoutAutoRefresh(function(GenericModel&Proxy $object) { $object->setProp1('new'); @@ -116,7 +118,7 @@ public function can_disable_and_enable_proxy_auto_refreshing_with_callback(): vo }); $this->assertSame('new 2', $object->getProp1()); - $this->factory()->repository()->assert()->exists(['prop1' => 'new 2']); + static::factory()->repository()->assert()->exists(['prop1' => 'new 2']); } /** @@ -124,16 +126,16 @@ public function can_disable_and_enable_proxy_auto_refreshing_with_callback(): vo */ public function can_manually_refresh_via_proxy(): void { - $object = $this->factory()->create()->_disableAutoRefresh(); + $object = static::factory()->create()->_disableAutoRefresh(); // initial data $this->assertSame('default1', $object->getProp1()); - $this->factory()->repository()->assert()->exists(['prop1' => 'default1']); + static::factory()->repository()->assert()->exists(['prop1' => 'default1']); self::ensureKernelShutdown(); // modify and save title "externally" - $ext = $this->factory()::first(); + $ext = static::factory()::first(); $ext->setProp1('external'); $ext->_save(); @@ -143,7 +145,7 @@ public function can_manually_refresh_via_proxy(): void // "calling method" triggers auto-refresh $this->assertSame('external', $object->getProp1()); - $this->factory()->repository()->assert()->exists(['prop1' => 'external']); + static::factory()->repository()->assert()->exists(['prop1' => 'external']); } /** @@ -151,16 +153,16 @@ public function can_manually_refresh_via_proxy(): void */ public function proxy_auto_refreshes(): void { - $object = $this->factory()->create(); + $object = static::factory()->create(); // initial data $this->assertSame('default1', $object->getProp1()); - $this->factory()->repository()->assert()->exists(['prop1' => 'default1']); + static::factory()->repository()->assert()->exists(['prop1' => 'default1']); self::ensureKernelShutdown(); // modify and save title "externally" - $ext = $this->factory()::first(); + $ext = static::factory()::first(); $ext->setProp1('external'); $ext->_save(); @@ -168,7 +170,7 @@ public function proxy_auto_refreshes(): void // "calling method" triggers auto-refresh $this->assertSame('external', $object->getProp1()); - $this->factory()->repository()->assert()->exists(['prop1' => 'external']); + static::factory()->repository()->assert()->exists(['prop1' => 'external']); } /** @@ -176,21 +178,21 @@ public function proxy_auto_refreshes(): void */ public function cannot_auto_refresh_proxy_if_changes(): void { - $object = $this->factory()->create(); + $object = static::factory()->create(); // initial data $this->assertSame('default1', $object->getProp1()); - $this->factory()->repository()->assert()->exists(['prop1' => 'default1']); + static::factory()->repository()->assert()->exists(['prop1' => 'default1']); $object->setProp1('new'); try { $object->setProp1('new 1'); } catch (\RuntimeException) { - $this->factory()->repository()->assert()->exists(['prop1' => 'default1']); + static::factory()->repository()->assert()->exists(['prop1' => 'default1']); $object->_save(); $this->assertSame('new', $object->getProp1()); - $this->factory()->repository()->assert()->exists(['prop1' => 'new']); + static::factory()->repository()->assert()->exists(['prop1' => 'new']); return; } @@ -203,11 +205,11 @@ public function cannot_auto_refresh_proxy_if_changes(): void */ public function can_access_repository_from_proxy(): void { - $object = $this->factory()::createOne(); + $object = static::factory()::createOne(); $object = $object->_repository()->findOneBy(['prop1' => 'default1']); - $this->assertInstanceOf($this->factory()::class(), $object); + $this->assertInstanceOf(static::factory()::class(), $object); } /** @@ -215,7 +217,7 @@ public function can_access_repository_from_proxy(): void */ public function can_force_set_and_get_proxy(): void { - $object = $this->factory()::createOne(); + $object = static::factory()::createOne(); $this->assertSame('default1', $object->_get('prop1')); @@ -229,7 +231,7 @@ public function can_force_set_and_get_proxy(): void */ public function can_get_real_object_even_if_modified(): void { - $object = $this->factory()->create(); + $object = static::factory()->create(); $object->setProp1('foo'); self::assertInstanceOf(GenericModel::class, $real = $object->_real()); @@ -246,7 +248,7 @@ public function can_create_object_with_readonly_properties(): void $objectWithReadOnly = $factory::createOne([ 'prop' => 1, 'embedded' => factory(Embeddable::class, ['prop1' => 'value1']), - 'date' => new \DateTimeImmutable() + 'date' => new \DateTimeImmutable(), ]); $objectWithReadOnly->_refresh(); @@ -259,7 +261,7 @@ public function can_create_object_with_readonly_properties(): void */ public function can_delete_proxified_object_and_still_access_its_methods(): void { - $object = $this->factory()->create(); + $object = static::factory()->create(); $object->_delete(); $this->assertSame('default1', $object->getProp1()); @@ -270,9 +272,9 @@ public function can_delete_proxified_object_and_still_access_its_methods(): void */ public function can_use_after_persist_with_attributes(): void { - $object = $this->factory() + $object = static::factory() ->instantiateWith(Instantiator::withConstructor()->allowExtra('extra')) - ->afterPersist(function (GenericModel $object, array $attributes) { + ->afterPersist(function(GenericModel $object, array $attributes) { $object->setProp1($attributes['extra']); }) ->create(['extra' => $value = 'value set with after persist']); @@ -280,10 +282,31 @@ public function can_use_after_persist_with_attributes(): void $this->assertSame($value, $object->getProp1()); } + /** + * @test + */ + public function can_use_after_persist_with_attributes_added_in_before_instantiate(): void + { + $value = 'value set with before instantiate'; + $object = $this->factory() + ->instantiateWith(Instantiator::withConstructor()->allowExtra('extra')) + ->beforeInstantiate(function(array $attributes) use ($value) { + $attributes['extra'] = $value; + + return $attributes; + }) + ->afterPersist(function(GenericModel $object, array $attributes) { + $object->setProp1($attributes['extra']); + }) + ->create(); + + $this->assertSame($value, $object->getProp1()); + } + /** * @return PersistentProxyObjectFactory */ - abstract protected function factory(): PersistentProxyObjectFactory; // @phpstan-ignore method.childReturnType + abstract protected static function factory(): PersistentProxyObjectFactory; /** * @return PersistentProxyObjectFactory diff --git a/tests/Integration/Persistence/PersistenceManagerTestCase.php b/tests/Integration/Persistence/PersistenceManagerTestCase.php new file mode 100644 index 000000000..f8d2e3992 --- /dev/null +++ b/tests/Integration/Persistence/PersistenceManagerTestCase.php @@ -0,0 +1,42 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Tests\Integration\Persistence; + +use Doctrine\Persistence\ObjectManager; +use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; +use Zenstruck\Foundry\Configuration; +use Zenstruck\Foundry\Test\Factories; + +abstract class PersistenceManagerTestCase extends KernelTestCase +{ + use Factories; + + /** + * @test + */ + public function it_can_test_if_object_with_uuid_is_persisted(): void + { + $object = $this->createObject(); + + $this->objectManager()->persist($object); + + self::assertFalse( + Configuration::instance()->persistence()->isPersisted($object) + ); + } + + abstract protected static function createObject(): object; + + abstract protected static function objectManager(): ObjectManager; +} diff --git a/tests/Integration/Persistence/StoryTest.php b/tests/Integration/Persistence/StoryTest.php index 573288aa9..0d17f9b61 100644 --- a/tests/Integration/Persistence/StoryTest.php +++ b/tests/Integration/Persistence/StoryTest.php @@ -16,9 +16,7 @@ use Zenstruck\Foundry\Story; use Zenstruck\Foundry\Test\Factories; use Zenstruck\Foundry\Test\ResetDatabase; -use Zenstruck\Foundry\Tests\Fixture\Document\GlobalDocument; use Zenstruck\Foundry\Tests\Fixture\Entity\GenericEntity; -use Zenstruck\Foundry\Tests\Fixture\Entity\GlobalEntity; use Zenstruck\Foundry\Tests\Fixture\Factories\Document\GenericDocumentFactory; use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\GenericEntityFactory; use Zenstruck\Foundry\Tests\Fixture\Model\GenericModel; @@ -27,12 +25,9 @@ use Zenstruck\Foundry\Tests\Fixture\Stories\DocumentStory; use Zenstruck\Foundry\Tests\Fixture\Stories\EntityPoolStory; use Zenstruck\Foundry\Tests\Fixture\Stories\EntityStory; -use Zenstruck\Foundry\Tests\Fixture\Stories\GlobalStory; use Zenstruck\Foundry\Tests\Fixture\Stories\ObjectStory; use Zenstruck\Foundry\Tests\Fixture\Stories\PersistenceDisabledStory; -use function Zenstruck\Foundry\Persistence\repository; - /** * @author Kevin Bond */ @@ -72,44 +67,6 @@ public function stories_only_loaded_once(string $story, string $factory): void $factory::repository()->assert()->count(2); } - /** - * @test - */ - public function global_stories_are_loaded(): void - { - if (!\getenv('DATABASE_URL') && !\getenv('MONGO_URL')) { - $this->markTestSkipped('No persistence enabled.'); - } - - if (\getenv('DATABASE_URL')) { - repository(GlobalEntity::class)->assert()->count(2); - } - - if (\getenv('MONGO_URL')) { - repository(GlobalDocument::class)->assert()->count(2); - } - } - - /** - * @test - */ - public function global_stories_cannot_be_loaded_again(): void - { - if (!\getenv('DATABASE_URL') && !\getenv('MONGO_URL')) { - $this->markTestSkipped('No persistence enabled.'); - } - - GlobalStory::load(); - - if (\getenv('DATABASE_URL')) { - repository(GlobalEntity::class)->assert()->count(2); - } - - if (\getenv('MONGO_URL')) { - repository(GlobalDocument::class)->assert()->count(2); - } - } - /** * @param class-string $story * diff --git a/tests/Integration/ResetDatabase/GlobalStoryTest.php b/tests/Integration/ResetDatabase/GlobalStoryTest.php new file mode 100644 index 000000000..c6d88124d --- /dev/null +++ b/tests/Integration/ResetDatabase/GlobalStoryTest.php @@ -0,0 +1,54 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Tests\Integration\ResetDatabase; + +use Zenstruck\Foundry\Tests\Fixture\Document\GlobalDocument; +use Zenstruck\Foundry\Tests\Fixture\Entity\GlobalEntity; +use Zenstruck\Foundry\Tests\Fixture\FoundryTestKernel; +use Zenstruck\Foundry\Tests\Fixture\Stories\GlobalStory; + +use function Zenstruck\Foundry\Persistence\repository; + +final class GlobalStoryTest extends ResetDatabaseTestCase +{ + /** + * @test + */ + public function global_stories_are_loaded(): void + { + if (FoundryTestKernel::hasORM()) { + repository(GlobalEntity::class)->assert()->count(2); + } + + if (FoundryTestKernel::hasMongo()) { + repository(GlobalDocument::class)->assert()->count(2); + } + } + + /** + * @test + */ + public function global_stories_cannot_be_loaded_again(): void + { + GlobalStory::load(); + + if (FoundryTestKernel::hasORM()) { + repository(GlobalEntity::class)->assert()->count(2); + } + + if (FoundryTestKernel::hasMongo()) { + repository(GlobalDocument::class)->assert()->count(2); + } + } +} diff --git a/tests/Integration/ResetDatabase/OrmEdgeCaseTest.php b/tests/Integration/ResetDatabase/OrmEdgeCaseTest.php new file mode 100644 index 000000000..4cae04a76 --- /dev/null +++ b/tests/Integration/ResetDatabase/OrmEdgeCaseTest.php @@ -0,0 +1,58 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Tests\Integration\ResetDatabase; + +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\RequiresPhpunit; +use PHPUnit\Framework\Attributes\Test; +use Zenstruck\Foundry\Tests\Fixture\DoctrineCascadeRelationship\ChangesEntityRelationshipCascadePersist; +use Zenstruck\Foundry\Tests\Fixture\DoctrineCascadeRelationship\UsingRelationships; +use Zenstruck\Foundry\Tests\Fixture\Entity\EdgeCases\RelationshipWithGlobalEntity; +use Zenstruck\Foundry\Tests\Fixture\Entity\GlobalEntity; +use Zenstruck\Foundry\Tests\Fixture\Stories\GlobalStory; + +use function Zenstruck\Foundry\Persistence\flush_after; +use function Zenstruck\Foundry\Persistence\persistent_factory; + +final class OrmEdgeCaseTest extends ResetDatabaseTestCase +{ + use ChangesEntityRelationshipCascadePersist; + + /** @test */ + #[Test] + #[DataProvider('provideCascadeRelationshipsCombinations')] + #[UsingRelationships(RelationshipWithGlobalEntity\RelationshipWithGlobalEntity::class, ['globalEntity'])] + #[RequiresPhpunit('>=11.4')] + public function it_can_use_flush_after_and_entity_from_global_state(): void + { + $relationshipWithGlobalEntityFactory = persistent_factory(RelationshipWithGlobalEntity\RelationshipWithGlobalEntity::class); + $globalEntitiesCount = persistent_factory(GlobalEntity::class)::repository()->count(); + + flush_after(function() use ($relationshipWithGlobalEntityFactory) { + $relationshipWithGlobalEntityFactory->create(['globalEntity' => GlobalStory::globalEntityProxy()]); + $relationshipWithGlobalEntityFactory->create(['globalEntity' => GlobalStory::globalEntity()]); + }); + + // assert no extra GlobalEntity have been created + persistent_factory(GlobalEntity::class)::assert()->count($globalEntitiesCount); + + $relationshipWithGlobalEntityFactory::assert()->count(2); + + $entity = $relationshipWithGlobalEntityFactory::repository()->first(); + self::assertSame(GlobalStory::globalEntity(), $entity?->getGlobalEntity()); + + $entity = $relationshipWithGlobalEntityFactory::repository()->last(); + self::assertSame(GlobalStory::globalEntity(), $entity?->getGlobalEntity()); + } +} diff --git a/tests/Integration/ResetDatabase/ResetDatabaseTest.php b/tests/Integration/ResetDatabase/ResetDatabaseTest.php new file mode 100644 index 000000000..304a83e09 --- /dev/null +++ b/tests/Integration/ResetDatabase/ResetDatabaseTest.php @@ -0,0 +1,157 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Tests\Integration\ResetDatabase; + +use Symfony\Bundle\FrameworkBundle\Console\Application; +use Symfony\Component\Console\Input\ArrayInput; +use Symfony\Component\Console\Output\BufferedOutput; +use Zenstruck\Foundry\Persistence\PersistenceManager; +use Zenstruck\Foundry\Tests\Fixture\EntityInAnotherSchema\Article; +use Zenstruck\Foundry\Tests\Fixture\Factories\Document\GenericDocumentFactory; +use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\GenericEntityFactory; +use Zenstruck\Foundry\Tests\Fixture\FoundryTestKernel; +use Zenstruck\Foundry\Tests\Fixture\ResetDatabase\MongoResetterDecorator; +use Zenstruck\Foundry\Tests\Fixture\ResetDatabase\OrmResetterDecorator; + +use function Zenstruck\Foundry\Persistence\persist; +use function Zenstruck\Foundry\Persistence\repository; + +/** + * @author Nicolas PHILIPPE + */ +final class ResetDatabaseTest extends ResetDatabaseTestCase +{ + /** + * @test + */ + public function it_generates_valid_schema(): void + { + $application = new Application(self::bootKernel()); + $application->setAutoExit(false); + + $exit = $application->run( + new ArrayInput(['command' => 'doctrine:schema:validate', '-v' => true]), + $output = new BufferedOutput() + ); + + if (FoundryTestKernel::usesMigrations()) { + // The command actually fails, because of a bug in doctrine ORM 3! + // https://github.com/doctrine/migrations/issues/1406 + self::assertSame(2, $exit, \sprintf('Schema is not valid: %s', $commandOutput = $output->fetch())); + self::assertStringContainsString('1 schema diff(s) detected', $commandOutput); + self::assertStringContainsString('DROP TABLE doctrine_migration_versions', $commandOutput); + } else { + self::assertSame(0, $exit, \sprintf('Schema is not valid: %s', $output->fetch())); + } + } + + /** + * @test + */ + public function it_can_store_object(): void + { + if (FoundryTestKernel::hasORM()) { + GenericEntityFactory::assert()->count(0); + GenericEntityFactory::createOne(); + GenericEntityFactory::assert()->count(1); + } + + if (FoundryTestKernel::hasMongo()) { + GenericDocumentFactory::assert()->count(0); + GenericDocumentFactory::createOne(); + GenericDocumentFactory::assert()->count(1); + } + } + + /** + * @test + * @depends it_can_store_object + */ + public function it_still_starts_from_fresh_db(): void + { + if (FoundryTestKernel::hasORM()) { + GenericEntityFactory::assert()->count(0); + } + + if (FoundryTestKernel::hasMongo()) { + GenericDocumentFactory::assert()->count(0); + } + } + + /** + * @test + */ + public function can_create_object_in_another_schema(): void + { + if (!\str_starts_with(\getenv('DATABASE_URL') ?: '', 'postgresql')) { + self::markTestSkipped('PostgreSQL needed.'); + } + + persist(Article::class, ['title' => 'Hello World!']); + repository(Article::class)->assert()->count(1); + } + + /** + * @test + */ + public function can_extend_orm_reset_mechanism_first(): void + { + if (!FoundryTestKernel::hasORM()) { + self::markTestSkipped('ORM needed.'); + } + + self::assertTrue(OrmResetterDecorator::$calledBeforeFirstTest); + + if (PersistenceManager::isOrmOnly() && FoundryTestKernel::usesDamaDoctrineTestBundle()) { + // in this case, the resetBeforeEachTest() method is never called + self::assertFalse(OrmResetterDecorator::$calledBeforeEachTest); + } else { + self::assertTrue(OrmResetterDecorator::$calledBeforeEachTest); + } + + OrmResetterDecorator::reset(); + } + + /** + * @test + * @depends can_extend_orm_reset_mechanism_first + */ + public function can_extend_orm_reset_mechanism_second(): void + { + if (!FoundryTestKernel::hasORM()) { + self::markTestSkipped('ORM needed.'); + } + + self::assertFalse(OrmResetterDecorator::$calledBeforeFirstTest); + + if (PersistenceManager::isOrmOnly() && FoundryTestKernel::usesDamaDoctrineTestBundle()) { + // in this case, the resetBeforeEachTest() method is never called + self::assertFalse(OrmResetterDecorator::$calledBeforeEachTest); + } else { + self::assertTrue(OrmResetterDecorator::$calledBeforeEachTest); + } + } + + /** + * @test + */ + public function can_extend_mongo_reset_mechanism_first(): void + { + if (!FoundryTestKernel::hasMongo()) { + self::markTestSkipped('Mongo needed.'); + } + + self::assertTrue(MongoResetterDecorator::$calledBeforeEachTest); + } +} diff --git a/tests/Integration/ResetDatabase/ResetDatabaseTestCase.php b/tests/Integration/ResetDatabase/ResetDatabaseTestCase.php new file mode 100644 index 000000000..f09625cab --- /dev/null +++ b/tests/Integration/ResetDatabase/ResetDatabaseTestCase.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\Tests\Integration\ResetDatabase; + +use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; +use Zenstruck\Foundry\Test\Factories; +use Zenstruck\Foundry\Test\ResetDatabase; +use Zenstruck\Foundry\Tests\Fixture\ResetDatabase\ResetDatabaseTestKernel; + +abstract class ResetDatabaseTestCase extends KernelTestCase +{ + use Factories, ResetDatabase; + + protected static function getKernelClass(): string + { + return ResetDatabaseTestKernel::class; + } +} diff --git a/tests/Unit/FactoryTest.php b/tests/Unit/FactoryTest.php index 47141ec99..925cde61a 100644 --- a/tests/Unit/FactoryTest.php +++ b/tests/Unit/FactoryTest.php @@ -19,13 +19,13 @@ use Zenstruck\Foundry\Persistence\Proxy; use Zenstruck\Foundry\Test\Factories; use Zenstruck\Foundry\Test\UnitTestConfig; -use Zenstruck\Foundry\Tests\Fixture\Entity\Category\StandardCategory; +use Zenstruck\Foundry\Tests\Fixture\Entity\Category; use Zenstruck\Foundry\Tests\Fixture\Entity\Contact; use Zenstruck\Foundry\Tests\Fixture\Entity\GenericEntity; use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\Address\ProxyAddressFactory; -use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\Category\StandardCategoryFactory; +use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\Category\CategoryFactory; +use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\Contact\ContactFactory; use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\Contact\ProxyContactFactory; -use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\Contact\StandardContactFactory; use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\GenericProxyEntityFactory; use Zenstruck\Foundry\Tests\Fixture\Object1; @@ -64,7 +64,7 @@ public function can_register_custom_faker(): void public function can_use_arrays_for_attribute_values(): void { $object = new class { - public mixed $value; + public mixed $value = null; }; $factory = factory($object::class)->create(['value' => ['foo' => 'bar']]); @@ -116,7 +116,7 @@ public function can_register_default_instantiator(): void public function proxy_attributes_can_be_used_in_unit_test(): void { $object = ProxyContactFactory::createOne([ - 'category' => proxy(new StandardCategory('name')), + 'category' => proxy(new Category('name')), 'address' => ProxyAddressFactory::new(), ]); @@ -128,11 +128,11 @@ public function proxy_attributes_can_be_used_in_unit_test(): void */ public function instantiating_with_factory_attribute_instantiates_the_factory(): void { - $object = StandardContactFactory::createOne([ - 'category' => StandardCategoryFactory::new(), + $object = ContactFactory::createOne([ + 'category' => CategoryFactory::new(), ]); - $this->assertInstanceOf(StandardCategory::class, $object->getCategory()); + $this->assertInstanceOf(Category::class, $object->getCategory()); } /** @@ -141,9 +141,9 @@ public function instantiating_with_factory_attribute_instantiates_the_factory(): public function instantiating_with_proxy_attribute_normalizes_to_underlying_object(): void { $object = ProxyContactFactory::createOne([ - 'category' => proxy(new StandardCategory('name')), + 'category' => proxy(new Category('name')), ]); - $this->assertInstanceOf(StandardCategory::class, $object->getCategory()); + $this->assertInstanceOf(Category::class, $object->getCategory()); } } diff --git a/tests/Unit/LazyValueTest.php b/tests/Unit/LazyValueTest.php index 55475503d..95a580310 100644 --- a/tests/Unit/LazyValueTest.php +++ b/tests/Unit/LazyValueTest.php @@ -57,18 +57,16 @@ public function can_handle_nested_lazy_values(): void */ public function can_handle_array_with_lazy_values(): void { - $value = LazyValue::new(function() { - return [ - 5, - LazyValue::new(fn() => 'foo'), - 6, - 'foo' => [ - 'bar' => 7, - 'baz' => LazyValue::new(fn() => 'foo'), - ], - [8, LazyValue::new(fn() => 'foo')], - ]; - }); + $value = LazyValue::new(fn() => [ + 5, + LazyValue::new(fn() => 'foo'), + 6, + 'foo' => [ + 'bar' => 7, + 'baz' => LazyValue::new(fn() => 'foo'), + ], + [8, LazyValue::new(fn() => 'foo')], + ]); $this->assertSame([5, 'foo', 6, 'foo' => ['bar' => 7, 'baz' => 'foo'], [8, 'foo']], $value()); } diff --git a/tests/Unit/ObjectFactoryTest.php b/tests/Unit/ObjectFactoryTest.php index 0396ea515..ffb33dc96 100644 --- a/tests/Unit/ObjectFactoryTest.php +++ b/tests/Unit/ObjectFactoryTest.php @@ -371,21 +371,21 @@ public static function sequenceDataProvider(): iterable ]; yield 'sequence as callable which returns array' => [ - static fn() => array_map( + static fn() => \array_map( static fn(int $i) => ['prop1' => "foo{$i}", 'prop2' => "bar{$i}"], - range(1, 2) - ) + \range(1, 2) + ), ]; yield 'sequence as iterable which returns generator' => [ - static function () { - foreach (range(1, 2) as $i) { + static function() { + foreach (\range(1, 2) as $i) { yield [ 'prop1' => "foo{$i}", 'prop2' => "bar{$i}", ]; } - } + }, ]; } diff --git a/tests/Unit/Persistence/ProxyGeneratorTest.php b/tests/Unit/Persistence/ProxyGeneratorTest.php index 0a5beae98..068d9a5d3 100644 --- a/tests/Unit/Persistence/ProxyGeneratorTest.php +++ b/tests/Unit/Persistence/ProxyGeneratorTest.php @@ -11,7 +11,7 @@ * file that was distributed with this source code. */ -namespace Unit\Persistence; +namespace Zenstruck\Foundry\Tests\Unit\Persistence; use PHPUnit\Framework\TestCase; use Zenstruck\Foundry\Persistence\ProxyGenerator; diff --git a/tests/bootstrap-reset-database.php b/tests/bootstrap-reset-database.php new file mode 100644 index 000000000..3c800926b --- /dev/null +++ b/tests/bootstrap-reset-database.php @@ -0,0 +1,47 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Symfony\Component\Dotenv\Dotenv; +use Symfony\Component\Filesystem\Filesystem; +use Zenstruck\Foundry\Tests\Fixture\FoundryTestKernel; +use Zenstruck\Foundry\Tests\Fixture\ResetDatabase\ResetDatabaseTestKernel; + +use function Zenstruck\Foundry\application; +use function Zenstruck\Foundry\runCommand; + +require \dirname(__DIR__).'/vendor/autoload.php'; + +$fs = new Filesystem(); + +$fs->remove(__DIR__.'/../var/cache'); + +(new Dotenv())->usePutenv()->loadEnv(__DIR__.'/../.env'); + +if (FoundryTestKernel::usesMigrations()) { + $fs->mkdir(__DIR__.'/../var/cache/Migrations'); + + $kernel = new ResetDatabaseTestKernel('test', true); + $kernel->boot(); + + $application = application($kernel); + + runCommand($application, 'doctrine:database:drop --if-exists --force', canFail: true); + runCommand($application, 'doctrine:database:create', canFail: true); + + $configuration = ''; + if (\getenv('MIGRATION_CONFIGURATION_FILE')) { + $configuration = '--configuration '.\getcwd().'/'.\getenv('MIGRATION_CONFIGURATION_FILE'); + } + runCommand($application, "doctrine:migrations:diff {$configuration}"); + runCommand($application, 'doctrine:database:drop --force', canFail: true); + + $kernel->shutdown(); +} diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 03193ea6a..c8aa9d218 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -9,47 +9,13 @@ * file that was distributed with this source code. */ -use Symfony\Bundle\FrameworkBundle\Console\Application; -use Symfony\Component\Console\Input\StringInput; -use Symfony\Component\Console\Output\NullOutput; use Symfony\Component\Dotenv\Dotenv; -use Symfony\Component\ErrorHandler\ErrorHandler; use Symfony\Component\Filesystem\Filesystem; -use Symfony\Component\Finder\Finder; -use Zenstruck\Foundry\ORM\ResetDatabase\ResetDatabaseMode; -use Zenstruck\Foundry\Tests\Fixture\TestKernel; require \dirname(__DIR__).'/vendor/autoload.php'; $fs = new Filesystem(); -$fs->remove(__DIR__.'/../var'); +$fs->remove(__DIR__.'/../var/cache'); (new Dotenv())->usePutenv()->loadEnv(__DIR__.'/../.env'); - -if (\getenv('DATABASE_URL') && ResetDatabaseMode::MIGRATE->value === \getenv('DATABASE_RESET_MODE')) { - $fs->remove(__DIR__.'/Fixture/Migrations'); - $fs->mkdir(__DIR__.'/Fixture/Migrations'); - - $kernel = new TestKernel('test', true); - $kernel->boot(); - - $application = new Application($kernel); - $application->setAutoExit(false); - - $application->run(new StringInput('doctrine:database:drop --if-exists --force'), new NullOutput()); - $application->run(new StringInput('doctrine:database:create'), new NullOutput()); - $application->run(new StringInput('doctrine:migrations:diff'), new NullOutput()); - $application->run(new StringInput('doctrine:database:drop --force'), new NullOutput()); - - // restore custom migrations - // this must be after "doctrine:migrations:diff" otherwise - // Doctrine is not able to run its diff command - foreach ((new Finder())->files()->in(__DIR__.'/Fixture/CustomMigrations') as $customMigrationFile) { - $fs->copy($customMigrationFile->getRealPath(), __DIR__.'/Fixture/Migrations/'.$customMigrationFile->getFilename()); - } - - $kernel->shutdown(); -} - -\set_exception_handler([new ErrorHandler(), 'handleException']);