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 936e3c130..f8d084949 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,42 +8,56 @@ on: jobs: tests: - name: P:${{ matrix.php }}, S:${{ matrix.symfony }}, D:${{ matrix.database }}, PU:${{ matrix.phpunit || 9 }}${{ matrix.deps == 'lowest' && ' (lowest)' || '' }}${{ matrix.without-dama == 0 && contains(matrix.database, 'sql') && ' (dama)' || '' }}${{ matrix.use-phpunit-extension == 1 && ' (phpunit extension)' || '' }} + 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, 8.4 ] + php: [ 8.2, 8.3, 8.4 ] symfony: [ 6.4.*, 7.1.*, 7.2.* ] - database: [ mysql, mongo ] + database: [ mysql|mongo ] + phpunit: [ 11 ] # default values: # deps: [ highest ] - # without-dama: [ 0 ] # use-phpunit-extension: [ 0 ] - # phpunit: [ 9 ] exclude: - {php: 8.1, symfony: 7.1.*} - {php: 8.1, symfony: 7.2.*} include: - - {php: 8.3, symfony: '*', database: none} - - {php: 8.3, symfony: '*', database: mysql|mongo} - - {php: 8.3, symfony: '*', database: pgsql|mongo} - - {php: 8.3, symfony: '*', database: pgsql, without-dama: 1} - - {php: 8.3, symfony: '*', database: sqlite, without-dama: 1} - - {php: 8.3, symfony: '*', database: sqlite, without-dama: 1, deps: lowest} - - {php: 8.3, symfony: '*', database: mysql, deps: lowest} - - {php: 8.3, symfony: '*', database: mysql|mongo, phpunit: 10} - - {php: 8.3, symfony: '*', database: mysql|mongo, phpunit: 11} - - {php: 8.3, symfony: '*', database: mysql|mongo, use-phpunit-extension: 1, phpunit: 11} - - {php: 8.3, symfony: '*', database: mysql|mongo, use-phpunit-extension: 1, phpunit: 11, without-dama: 1} + # 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.without-dama == 0 && 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 || 9 }} + PHPUNIT_VERSION: ${{ matrix.phpunit }} services: postgres: image: ${{ contains(matrix.database, 'pgsql') && 'postgres:15' || '' }} @@ -89,28 +103,28 @@ jobs: run: ./phpunit shell: bash - test-reset-database-with-migration: - name: Test migration - D:${{ matrix.database }} ${{ matrix.use-dama == 1 && ' (dama)' || '' }} ${{ contains(matrix.with-migration-configuration-file, 'transactional') && '(configuration file transactional)' || contains(matrix.with-migration-configuration-file, 'configuration') && '(configuration file)' || '' }} + 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 ] + database: [ mysql, pgsql, sqlite, mysql|mongo ] use-dama: [ 0, 1 ] - with-migration-configuration-file: - - '' - - 'tests/Fixture/MigrationTests/configs/migration-configuration.php' - - 'tests/Fixture/MigrationTests/configs/migration-configuration-transactional.php' - exclude: - # there is currently a bug with MySQL and transactional migrations - - database: mysql - with-migration-configuration-file: 'tests/Fixture/MigrationTests/configs/migration-configuration-transactional.php' + 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: '' + 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 }} - WITH_MIGRATION_CONFIGURATION_FILE: ${{ matrix.with-migration-configuration-file }} - PHPUNIT_VERSION: 9 + 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' || '' }} @@ -125,6 +139,10 @@ jobs: --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 @@ -139,7 +157,7 @@ jobs: - name: Install dependencies uses: ramsey/composer-install@v2 with: - dependency-versions: highest + dependency-versions: ${{ matrix.deps }} composer-options: --prefer-dist env: SYMFONY_REQUIRE: 7.1.* @@ -149,7 +167,12 @@ jobs: run: sudo /etc/init.d/mysql start - name: Test - run: ./phpunit --testsuite migrate --bootstrap tests/bootstrap-migrate.php + 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: @@ -274,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 @@ -294,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 6d5aba94f..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/MigrationTests/Migrations/ /tests/Fixture/Maker/tmp/ diff --git a/CHANGELOG.md b/CHANGELOG.md index eeca19095..cabfeb842 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,44 @@ # CHANGELOG +## [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 + ## [v2.3.1](https://github.com/zenstruck/foundry/releases/tag/v2.3.1) December 12th, 2024 - [v2.3.0...v2.3.1](https://github.com/zenstruck/foundry/compare/v2.3.0...v2.3.1) diff --git a/README.md b/README.md index ba954bc3d..153eb6871 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ $ ./phpunit # run "migrate" testsuite (with "migrate" reset database strategy) $ composer test-migrate # or -$ ./phpunit --testsuite migrate --bootstrap tests/bootstrap-migrate.php +$ ./phpunit --testsuite reset-database --bootstrap tests/bootstrap-reset-database.php ``` ### Overriding the default configuration @@ -74,9 +74,9 @@ 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 configuration migration, -# only relevant for "migrate" testsuite -WITH_MIGRATION_CONFIGURATION_FILE="tests/Fixture/MigrationTests/configs/migration-configuration.php" +# 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/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 490e79bb4..1be90a4c6 100644 --- a/composer.json +++ b/composer.json @@ -28,12 +28,13 @@ "require-dev": { "bamarni/composer-bin-plugin": "^1.8", "brianium/paratest": "^6|^7", - "dama/doctrine-test-bundle": "^7.0|^8.0", + "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", @@ -42,6 +43,7 @@ "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" }, @@ -61,7 +63,8 @@ "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", @@ -84,15 +87,15 @@ }, "scripts": { "test": [ - "@test-schema", - "@test-migrate" + "@test-main", + "@test-reset-database" ], - "test-schema": "./phpunit", - "test-migrate": "./phpunit --testsuite migrate --bootstrap tests/bootstrap-migrate.php" + "test-main": "./phpunit --testsuite main", + "test-reset-database": "./phpunit --testsuite reset-database --bootstrap tests/bootstrap-reset-database.php" }, "scripts-descriptions": { - "test-schema": "Test with schema reset", - "test-migrate": "Test with migrations reset" + "test-main": "Main test suite", + "test-reset-database": "Test reset database test suite" }, "minimum-stability": "dev", "prefer-stable": true 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/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 9a38cdff0..0a0aab2f3 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:: @@ -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; @@ -552,20 +564,24 @@ they were added. use Zenstruck\Foundry\Proxy; PostFactory::new() - ->beforeInstantiate(function(array $attributes): array { + ->beforeInstantiate(function(array $attributes, string $class, static $factory): array { // $attributes 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 $attributes['title'] = 'Different title'; return $attributes; // must return the final $attributes }) - ->afterInstantiate(function(Post $object, array $attributes): void { + ->afterInstantiate(function(Post $object, array $attributes, static $factory): void { // $object is the instantiated object // $attributes 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 $attributes, 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 + // $factory is the factory instance which creates the object }) // multiple events are allowed @@ -612,15 +628,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 +668,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): @@ -716,7 +746,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 +869,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 +1077,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 +1331,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:: @@ -1480,7 +1510,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 @@ -1489,7 +1521,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 ..................... @@ -1573,7 +1604,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. @@ -1623,10 +1654,11 @@ With PHPUnit Extension 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 + +* 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. :: @@ -1738,7 +1770,7 @@ Be sure your data provider returns only instances of ``Factory`` and you do not )->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: :: @@ -2212,8 +2244,9 @@ Foundry is shipped with an extension for PHPUnit. You can install it by modifyin 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) + +* support for the `#[WithStory] Attribute`_ +* ability to use ``Factory::create()`` in `PHPUnit Data Providers`_ (along with PHPUnit ^11.4) .. versionadded:: 2.2 diff --git a/phpstan.neon b/phpstan.neon index 5f677458e..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#' @@ -15,9 +28,46 @@ parameters: path: tests/ # We support both PHPUnit versions (this method changed in PHPUnit 10) - - message: '#Call to function method_exists\(\) with .* will always evaluate to false#' + - 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 b62d8710b..dfe4b3f60 100755 --- a/phpunit +++ b/phpunit @@ -16,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 @@ -63,7 +63,7 @@ case ${PHPUNIT_VERSION} in ;; "11") - PHPUNIT_EXEC="${PHPUNIT_EXEC} -c phpunit-10.xml.dist" + 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}"" diff --git a/phpunit-10.xml.dist b/phpunit-10.xml.dist index b97581549..43f815cc7 100644 --- a/phpunit-10.xml.dist +++ b/phpunit-10.xml.dist @@ -17,10 +17,10 @@ tests - tests/Integration/Migration/ResetDatabaseWithMigrationTest.php + tests/Integration/ResetDatabase - - tests/Integration/Migration/ResetDatabaseWithMigrationTest.php + + tests/Integration/ResetDatabase diff --git a/phpunit-paratest.xml.dist b/phpunit-paratest.xml.dist index 66b9f9a74..1f6aee78d 100644 --- a/phpunit-paratest.xml.dist +++ b/phpunit-paratest.xml.dist @@ -16,7 +16,7 @@ tests - tests/Integration/Migration/ResetDatabaseWithMigrationTest.php + tests/Integration/ResetDatabase @@ -26,6 +26,7 @@ + diff --git a/phpunit.xml.dist b/phpunit.xml.dist index c6b9f26cb..15de54d9a 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -19,10 +19,11 @@ tests - tests/Integration/Migration/ResetDatabaseWithMigrationTest.php + tests/Integration/ResetDatabase + tests/Integration/ForceFactoriesTraitUsage - - tests/Integration/Migration/ResetDatabaseWithMigrationTest.php + + tests/Integration/ResetDatabase diff --git a/src/Attribute/WithStory.php b/src/Attribute/WithStory.php index 8d2331b39..c0b305736 100644 --- a/src/Attribute/WithStory.php +++ b/src/Attribute/WithStory.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\Attribute; use Zenstruck\Foundry\Story; @@ -11,9 +20,8 @@ final class WithStory { public function __construct( /** @var class-string $story */ - public readonly 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 551d9feff..8e0dc219d 100644 --- a/src/Configuration.php +++ b/src/Configuration.php @@ -12,6 +12,7 @@ namespace Zenstruck\Foundry; use Faker; +use Zenstruck\Foundry\Exception\FactoriesTraitNotUsed; use Zenstruck\Foundry\Exception\FoundryNotBooted; use Zenstruck\Foundry\Exception\PersistenceDisabled; use Zenstruck\Foundry\Exception\PersistenceNotAvailable; @@ -67,9 +68,14 @@ 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.'); } } @@ -85,6 +91,8 @@ public static function instance(): self throw new FoundryNotBooted(); } + FactoriesTraitNotUsed::throwIfComingFromKernelTestCaseWithoutFactoriesTrait(); + return \is_callable(self::$instance) ? (self::$instance)() : self::$instance; } @@ -93,11 +101,13 @@ 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; diff --git a/src/Exception/FactoriesTraitNotUsed.php b/src/Exception/FactoriesTraitNotUsed.php new file mode 100644 index 000000000..10aeac530 --- /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, you must use the trait "%s" in your "%s" tests. This will throw an exception in 3.0.', + KernelTestCase::class, + $class + ); + } + } +} diff --git a/src/Factory.php b/src/Factory.php index 4707fa70b..c4489582a 100644 --- a/src/Factory.php +++ b/src/Factory.php @@ -25,6 +25,13 @@ */ abstract class Factory { + /** + * Memoization of normalized parameters. + * + * @internal + * @var Parameters|null + */ + protected ?array $normalizedParameters = null; /** @phpstan-var Attributes[] */ private array $attributes; @@ -34,7 +41,6 @@ public function __construct() } /** - * @return static * @phpstan-return static * @phpstan-param Attributes $attributes */ @@ -50,7 +56,10 @@ final public static function new(array|callable $attributes = []): static throw new \LogicException('Factories with dependencies (services) cannot be created before foundry is booted.', previous: $e); } - return $factory->initialize()->with($attributes); + return $factory + ->initializeInternal() + ->initialize() + ->with($attributes); } /** @@ -101,24 +110,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 +135,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 +157,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 +192,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 +208,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) ); } @@ -219,9 +236,9 @@ 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) + return \array_combine( + \array_keys($value), + \array_map($this->normalizeParameter(...), \array_fill(0, \count($value), $field), $value) ); } @@ -231,7 +248,7 @@ protected function normalizeParameter(string $field, mixed $value): mixed /** * @internal * - * @param FactoryCollection $collection + * @param FactoryCollection> $collection * * @return self[] */ diff --git a/src/FactoryCollection.php b/src/FactoryCollection.php index 7d9d6ee86..b2319f909 100644 --- a/src/FactoryCollection.php +++ b/src/FactoryCollection.php @@ -11,32 +11,52 @@ 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|\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 + * @phpstan-assert-if-true non-empty-list $potentialFactories * * @internal */ public static function accepts(mixed $potentialFactories): bool { - if (!is_array($potentialFactories) || count($potentialFactories) === 0 || !array_is_list($potentialFactories)) { + if (!\is_array($potentialFactories) || 0 === \count($potentialFactories) || !\array_is_list($potentialFactories)) { return false; } @@ -46,7 +66,7 @@ public static function accepts(mixed $potentialFactories): bool foreach ($potentialFactories as $potentialFactory) { if (!$potentialFactory instanceof ObjectFactory - || $potentialFactory::class() !== $potentialFactories[0]::class()) { + || $potentialFactories[0]::class() !== $potentialFactory::class()) { return false; } } @@ -55,9 +75,9 @@ public static function accepts(mixed $potentialFactories): bool } /** - * @param array $factories + * @param array $factories * - * @return self + * @return self * * @internal */ @@ -71,9 +91,9 @@ public static function fromFactoriesList(array $factories): self } /** - * @param Factory $factory + * @param TFactory $factory * - * @return self + * @return self */ public static function many(Factory $factory, int $count): self { @@ -81,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 { @@ -95,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 { @@ -115,7 +135,7 @@ public function create(array|callable $attributes = []): array } /** - * @return list> + * @return list */ public function all(): array { @@ -132,7 +152,16 @@ public function all(): array $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 @@ -141,7 +170,7 @@ public function getIterator(): \Traversable } /** - * @return iterable}> + * @return iterable */ public function asDataProvider(): iterable { 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..76148a4b5 100644 --- a/src/Maker/Factory/FactoryGenerator.php +++ b/src/Maker/Factory/FactoryGenerator.php @@ -108,7 +108,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); diff --git a/src/Maker/Factory/MakeFactoryData.php b/src/Maker/Factory/MakeFactoryData.php index 9fbac6973..fc9311349 100644 --- a/src/Maker/Factory/MakeFactoryData.php +++ b/src/Maker/Factory/MakeFactoryData.php @@ -36,7 +36,6 @@ final class MakeFactoryData /** @var list */ private array $methodsInPHPDoc; - // @phpstan-ignore-next-line public function __construct( private \ReflectionClass $object, private ClassNameDetails $factoryClassNameDetails, @@ -79,7 +78,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 +99,7 @@ public function getObjectFullyQualifiedClassName(): string return $this->object->getName(); } - public function getRepositoryReflectionClass(): ?\ReflectionClass // @phpstan-ignore missingType.generics + public function getRepositoryReflectionClass(): ?\ReflectionClass { return $this->repository; } @@ -178,7 +177,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."); } diff --git a/src/Maker/Factory/MakeFactoryPHPDocMethod.php b/src/Maker/Factory/MakeFactoryPHPDocMethod.php index 22ceff09d..b6ce55d41 100644 --- a/src/Maker/Factory/MakeFactoryPHPDocMethod.php +++ b/src/Maker/Factory/MakeFactoryPHPDocMethod.php @@ -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/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 cf934db88..2a1d14fd5 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,35 +25,25 @@ */ 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, $child, $field); - - if ($association) { - return new RelationshipMetadata( - isCascadePersist: $association['isCascadePersist'], - inverseField: $metadata->isSingleValuedAssociation($association['fieldName']) ? $association['fieldName'] : null, - isCollection: $metadata->isCollectionValuedAssociation($association['fieldName']), - isOneToOne: ClassMetadataInfo::ONE_TO_ONE === $association['type'] - ); - } - - $inversedAssociation = $this->getAssociationMapping($child, $parent, $field); + $inversedAssociation = $this->getAssociationMapping($parent, $child, $field); if (null === $inversedAssociation || !$metadata instanceof ClassMetadataInfo) { return null; } if (!\is_a( - $parent, + $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}\"]"); } + // exclude "owning" side of the association (owning OneToOne or ManyToOne) if (!\in_array( $inversedAssociation['type'], [ClassMetadataInfo::ONE_TO_MANY, ClassMetadataInfo::ONE_TO_ONE], @@ -65,13 +55,17 @@ public function relationshipMetadata(string $parent, string $child, string $fiel } $association = $metadata->getAssociationMapping($inversedAssociation['mappedBy']); + + // only keep *ToOne associations + if (!$metadata->isSingleValuedAssociation($association['fieldName'])) { + return null; + } + $inversedAssociationMetadata = $this->classMetadata($inversedAssociation['sourceEntity']); - return new RelationshipMetadata( - isCascadePersist: $inversedAssociation['isCascadePersist'], - inverseField: $metadata->isSingleValuedAssociation($association['fieldName']) ? $association['fieldName'] : null, + return new InverseRelationshipMetadata( + inverseField: $association['fieldName'], isCollection: $inversedAssociationMetadata->isCollectionValuedAssociation($inversedAssociation['fieldName']), - isOneToOne: ClassMetadataInfo::ONE_TO_ONE === $inversedAssociation['type'] ); } diff --git a/src/ORM/OrmV3PersistenceStrategy.php b/src/ORM/OrmV3PersistenceStrategy.php index 95e240cbb..6d8d187e3 100644 --- a/src/ORM/OrmV3PersistenceStrategy.php +++ b/src/ORM/OrmV3PersistenceStrategy.php @@ -17,53 +17,45 @@ use Doctrine\ORM\Mapping\ClassMetadata; use Doctrine\ORM\Mapping\InverseSideMapping; use Doctrine\ORM\Mapping\MappingException as ORMMappingException; -use Doctrine\ORM\Mapping\OneToOneAssociationMapping; 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, $child, $field); - - if ($association) { - return new RelationshipMetadata( - isCascadePersist: $association->isCascadePersist(), - inverseField: $metadata->isSingleValuedAssociation($association->fieldName) ? $association->fieldName : null, - isCollection: $association instanceof ToManyAssociationMapping, - isOneToOne: $association instanceof OneToOneAssociationMapping, - ); - } - - $inversedAssociation = $this->getAssociationMapping($child, $parent, $field); + $inversedAssociation = $this->getAssociationMapping($parent, $child, $field); if (null === $inversedAssociation || !$metadata instanceof ClassMetadata) { return null; } if (!\is_a( - $parent, + $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}\"]"); } + // exclude "owning" side of the association (owning OneToOne or ManyToOne) if (!$inversedAssociation instanceof InverseSideMapping) { return null; } $association = $metadata->getAssociationMapping($inversedAssociation->mappedBy); - return new RelationshipMetadata( - isCascadePersist: $inversedAssociation->isCascadePersist(), - inverseField: $metadata->isSingleValuedAssociation($association->fieldName) ? $association->fieldName : null, + // only keep *ToOne associations + if (!$metadata->isSingleValuedAssociation($association->fieldName)) { + return null; + } + + return new InverseRelationshipMetadata( + inverseField: $association->fieldName, isCollection: $inversedAssociation instanceof ToManyAssociationMapping, - isOneToOne: $inversedAssociation instanceof OneToOneAssociationMapping, ); } diff --git a/src/ORM/ResetDatabase/BaseOrmResetter.php b/src/ORM/ResetDatabase/BaseOrmResetter.php index 8f2bf840e..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; @@ -12,8 +21,6 @@ use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\HttpKernel\KernelInterface; -use Zenstruck\Foundry\ORM\DoctrineOrmVersionGuesser; - use function Zenstruck\Foundry\runCommand; /** @@ -60,9 +67,8 @@ final protected function dropAndResetDatabase(Application $application): void // let's only drop the .db file $dbPath = $connection->getParams()['path'] ?? null; - $fs = new Filesystem(); - if (DoctrineOrmVersionGuesser::isOrmV3() && $dbPath && $fs->exists($dbPath)) { - (new Filesystem())->remove($dbPath); + if ($dbPath && (new Filesystem())->exists($dbPath)) { + \file_put_contents($dbPath, ''); } continue; diff --git a/src/ORM/ResetDatabase/MigrateDatabaseResetter.php b/src/ORM/ResetDatabase/MigrateDatabaseResetter.php index 52a156980..8e3f321b8 100644 --- a/src/ORM/ResetDatabase/MigrateDatabaseResetter.php +++ b/src/ORM/ResetDatabase/MigrateDatabaseResetter.php @@ -2,10 +2,18 @@ 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\Bundle\FrameworkBundle\Console\Application; use Symfony\Component\HttpKernel\KernelInterface; use function Zenstruck\Foundry\application; @@ -25,12 +33,11 @@ public function __construct( Registry $registry, array $managers, array $connections, - ) - { + ) { parent::__construct($registry, $managers, $connections); } - final public function resetBeforeFirstTest(KernelInterface $kernel): void + public function resetBeforeFirstTest(KernelInterface $kernel): void { $this->resetWithMigration($kernel); } 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 12342b430..677f13787 100644 --- a/src/ORM/ResetDatabase/SchemaDatabaseResetter.php +++ b/src/ORM/ResetDatabase/SchemaDatabaseResetter.php @@ -33,7 +33,7 @@ public function resetBeforeFirstTest(KernelInterface $kernel): void $this->createSchema($application); } - public function doResetBeforeEachTest(KernelInterface $kernel): void + protected function doResetBeforeEachTest(KernelInterface $kernel): void { $application = application($kernel); 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..8d69933f4 100644 --- a/src/ObjectFactory.php +++ b/src/ObjectFactory.php @@ -24,10 +24,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 +46,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 +59,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 +80,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 +93,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 { diff --git a/src/PHPUnit/BuildStoryOnTestPrepared.php b/src/PHPUnit/BuildStoryOnTestPrepared.php index 8689dbfee..ff3ea9eb4 100644 --- a/src/PHPUnit/BuildStoryOnTestPrepared.php +++ b/src/PHPUnit/BuildStoryOnTestPrepared.php @@ -16,6 +16,7 @@ use PHPUnit\Event; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; use Zenstruck\Foundry\Attribute\WithStory; +use Zenstruck\Foundry\Exception\FactoriesTraitNotUsed; /** * @internal @@ -46,6 +47,8 @@ public function notify(Event\Test\Prepared $event): void 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(); } diff --git a/src/Persistence/RelationshipMetadata.php b/src/Persistence/InverseRelationshipMetadata.php similarity index 72% rename from src/Persistence/RelationshipMetadata.php rename to src/Persistence/InverseRelationshipMetadata.php index fe1df93a9..50b35c9ec 100644 --- a/src/Persistence/RelationshipMetadata.php +++ b/src/Persistence/InverseRelationshipMetadata.php @@ -16,13 +16,11 @@ * * @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 bool $isOneToOne, ) { } } diff --git a/src/Persistence/IsProxy.php b/src/Persistence/IsProxy.php index a49967f6b..0c140b2b4 100644 --- a/src/Persistence/IsProxy.php +++ b/src/Persistence/IsProxy.php @@ -25,7 +25,7 @@ * * @mixin LazyProxyTrait */ -trait IsProxy +trait IsProxy // @phpstan-ignore trait.unused { private static array $_autoRefresh = []; @@ -134,17 +134,10 @@ public function _initializeLazyObject(): void private function isPersisted(): bool { - try { - $this->_refresh(); - - return true; - } catch (RefreshObjectFailed $e) { - if ($e->objectWasDeleted()) { - return false; - } + $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 88979e6d2..aaf4d621f 100644 --- a/src/Persistence/PersistenceManager.php +++ b/src/Persistence/PersistenceManager.php @@ -75,6 +75,36 @@ public function save(object $object): object return $object; } + /** + * @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 * @@ -149,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 * @@ -191,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; + } } /** @@ -212,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 { @@ -229,7 +283,7 @@ public function metadataFor(string $class): ClassMetadata } /** - * @return iterable + * @return iterable> */ public function allMetadata(): iterable { @@ -273,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 f2aabca44..7267096c9 100644 --- a/src/Persistence/PersistentObjectFactory.php +++ b/src/Persistence/PersistentObjectFactory.php @@ -22,7 +22,7 @@ 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 @@ -34,13 +34,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 @@ -166,7 +166,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 } @@ -196,9 +196,15 @@ public function create(callable|array $attributes = []): object { $object = parent::create($attributes); + foreach ($this->tempAfterInstantiate as $callback) { + $callback($object); + } + + $this->tempAfterInstantiate = []; + $this->throwIfCannotCreateObject(); - if (!$this->isPersisting()) { + if (PersistMode::PERSIST !== $this->persistMode()) { return $object; } @@ -210,17 +216,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); @@ -232,7 +232,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; } @@ -240,13 +240,24 @@ final public function andPersist(): static final public function withoutPersisting(): static { $clone = clone $this; - $clone->persist = false; + $clone->persist = PersistMode::WITHOUT_PERSISTING; + + return $clone; + } + + /** + * @internal + */ + public function withPersistMode(PersistMode $persistMode): static + { + $clone = clone $this; + $clone->persist = $persistMode; return $clone; } /** - * @phpstan-param callable(T, Parameters):void $callback + * @phpstan-param callable(T, Parameters, static):void $callback */ final public function afterPersist(callable $callback): static { @@ -256,6 +267,20 @@ final public function afterPersist(callable $callback): static return $clone; } + /** + * @internal + */ + public function persistMode(): PersistMode + { + $config = Configuration::instance(); + + if (!$config->isPersistenceEnabled()) { + 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()) { @@ -263,41 +288,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) { $pm = Configuration::instance()->persistence(); - $relationshipMetadata = $pm->relationshipMetadata($value::class(), static::class(), $field); + $inversedRelationshipMetadata = $pm->inverseRelationshipMetadata(static::class(), $value::class(), $field); // handle inversed OneToOne - if ($relationshipMetadata - && $relationshipMetadata->isOneToOne - && !$relationshipMetadata->isCollection - && $inverseField = $relationshipMetadata->inverseField) { - // we create now the object to prevent "non-nullable" property errors, - // but we'll need to remove it once the current object is created - $inversedObject = unproxy($value->create()); - $this->tempAfterPersist[] = static function(object $object) use ($value, $inverseField, $pm, $inversedObject) { - // we cannot use the already created $inversedObject: - // because we must also remove its potential newly created owner (here: "$oldObj") - // but a cascade:["persist"] would remove too many things - $value->create([$inverseField => $object]); - $pm->refresh($object); - $oldObj = get($inversedObject, $inverseField); - delete($inversedObject); - if ($oldObj) { - delete($oldObj); // @phpstan-ignore argument.templateType - } + 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; } - - if (Configuration::instance()->persistence()->relationshipMetadata(static::class(), $value::class(), $field)?->isCascadePersist) { - $value->persist = false; - } } return unproxy(parent::normalizeParameter($field, $value)); @@ -311,10 +332,14 @@ 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) { + $inverseField = $inverseRelationshipMetadata->inverseField; + + $this->tempAfterInstantiate[] = static function(object $object) use ($collection, $inverseField, $field) { + $inverseObjects = $collection->withPersistMode(PersistMode::NO_PERSIST_BUT_SCHEDULE_FOR_INSERT)->create([$inverseField => $object]); + set($object, $field, unproxy($inverseObjects)); }; // creation delegated to afterPersist hook - return empty array here @@ -342,12 +367,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; } @@ -357,11 +399,28 @@ final protected function isPersisting(): bool { $config = Configuration::instance(); - if ($config->isPersistenceAvailable() && !$config->persistence()->isEnabled()) { + if (!$config->isPersistenceEnabled()) { return false; } - return $this->persist ?? $config->isPersistenceAvailable() && $config->persistence()->isEnabled() && $config->persistence()->autoPersist(static::class()); + return $this->persistMode()->isPersisting(); + } + + /** + * Schedule any new object for insert right after instantiation. + * @internal + */ + final protected function initializeInternal(): static + { + return $this->afterInstantiate( + static function(object $object, array $parameters, PersistentObjectFactory $factory): void { + if (!$factory->isPersisting()) { + return; + } + + Configuration::instance()->persistence()->scheduleForInsert($object); + } + ); } private function throwIfCannotCreateObject(): void diff --git a/src/Persistence/PersistentProxyObjectFactory.php b/src/Persistence/PersistentProxyObjectFactory.php index 226e80459..a0860a705 100644 --- a/src/Persistence/PersistentProxyObjectFactory.php +++ b/src/Persistence/PersistentProxyObjectFactory.php @@ -141,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/ProxyRepositoryDecorator.php b/src/Persistence/ProxyRepositoryDecorator.php index 21dfff8e9..578a05c1c 100644 --- a/src/Persistence/ProxyRepositoryDecorator.php +++ b/src/Persistence/ProxyRepositoryDecorator.php @@ -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 bc8b925ad..7dc8af29f 100644 --- a/src/Persistence/RepositoryDecorator.php +++ b/src/Persistence/RepositoryDecorator.php @@ -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 { 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 1f31d3abb..3b7436a2d 100644 --- a/src/Test/Factories.php +++ b/src/Test/Factories.php @@ -50,7 +50,7 @@ public static function _shutdownFoundry(): void */ public static function _bootForDataProvider(): void { - if (!\is_subclass_of(static::class, KernelTestCase::class)) { // @phpstan-ignore function.impossibleType + if (!\is_subclass_of(static::class, KernelTestCase::class)) { // @phpstan-ignore function.impossibleType, function.alreadyNarrowedType // unit test Configuration::bootForDataProvider(UnitTestConfig::build()); @@ -58,12 +58,12 @@ public static function _bootForDataProvider(): void } // integration test - Configuration::bootForDataProvider(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 }); } @@ -73,7 +73,7 @@ public static function _bootForDataProvider(): void */ public static function _shutdownAfterDataProvider(): void { - if (\is_subclass_of(static::class, KernelTestCase::class)) { // @phpstan-ignore function.impossibleType + 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 @@ -87,7 +87,7 @@ public static function _shutdownAfterDataProvider(): void */ private function _bootFoundry(): void { - if (!\is_subclass_of(static::class, KernelTestCase::class)) { // @phpstan-ignore function.impossibleType + if (!\is_subclass_of(static::class, KernelTestCase::class)) { // @phpstan-ignore function.impossibleType, function.alreadyNarrowedType // unit test Configuration::boot(UnitTestConfig::build()); @@ -95,12 +95,12 @@ private function _bootFoundry(): void } // integration test - Configuration::boot(static function() { + 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 static::getContainer()->get('.zenstruck_foundry.configuration'); // @phpstan-ignore staticMethod.notFound, return.type }); } @@ -128,7 +128,7 @@ private function _bootFoundry(): void */ private function _loadDataProvidedProxies(): void { - if (!\is_subclass_of(static::class, KernelTestCase::class)) { // @phpstan-ignore function.impossibleType + if (!\is_subclass_of(static::class, KernelTestCase::class)) { // @phpstan-ignore function.impossibleType, function.alreadyNarrowedType return; } diff --git a/src/Test/ResetDatabase.php b/src/Test/ResetDatabase.php index d9698b817..d17e8bf39 100644 --- a/src/Test/ResetDatabase.php +++ b/src/Test/ResetDatabase.php @@ -28,7 +28,7 @@ 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)); } @@ -45,7 +45,7 @@ public static function _resetDatabaseBeforeFirstTest(): 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..e8e188be6 100644 --- a/src/ZenstruckFoundryBundle.php +++ b/src/ZenstruckFoundryBundle.php @@ -19,8 +19,10 @@ use Symfony\Component\HttpKernel\Bundle\AbstractBundle; use Zenstruck\Foundry\Mongo\MongoResetter; 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 @@ -249,18 +251,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,11 +275,9 @@ 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'); } } 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/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/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 e7ea0849e..41627cd0d 100644 --- a/tests/Fixture/Entity/Address.php +++ b/tests/Fixture/Entity/Address.php @@ -17,9 +17,10 @@ /** * @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)] diff --git a/tests/Fixture/Entity/Address/CascadeAddress.php b/tests/Fixture/Entity/Address/CascadeAddress.php deleted file mode 100644 index 06fa8b613..000000000 --- a/tests/Fixture/Entity/Address/CascadeAddress.php +++ /dev/null @@ -1,27 +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\Address; - -use Doctrine\ORM\Mapping as ORM; -use Zenstruck\Foundry\Tests\Fixture\Entity\Address; -use Zenstruck\Foundry\Tests\Fixture\Entity\Contact; -use Zenstruck\Foundry\Tests\Fixture\Entity\Contact\CascadeContact; - -/** - * @author Kevin Bond - */ -#[ORM\Entity] -class CascadeAddress extends Address -{ - #[ORM\OneToOne(targetEntity: CascadeContact::class, mappedBy: 'address', cascade: ['persist', 'remove'])] - protected ?Contact $contact = null; -} diff --git a/tests/Fixture/Entity/Address/StandardAddress.php b/tests/Fixture/Entity/Address/StandardAddress.php deleted file mode 100644 index 45522e234..000000000 --- a/tests/Fixture/Entity/Address/StandardAddress.php +++ /dev/null @@ -1,27 +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\Address; - -use Doctrine\ORM\Mapping as ORM; -use Zenstruck\Foundry\Tests\Fixture\Entity\Address; -use Zenstruck\Foundry\Tests\Fixture\Entity\Contact; -use Zenstruck\Foundry\Tests\Fixture\Entity\Contact\StandardContact; - -/** - * @author Kevin Bond - */ -#[ORM\Entity] -class StandardAddress extends Address -{ - #[ORM\OneToOne(targetEntity: StandardContact::class, mappedBy: 'address')] - protected ?Contact $contact = null; -} 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 21b58e3f7..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, inversedBy: 'contact', 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 98fb91327..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, inversedBy: 'contact')] - #[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/InversedOneToOneWithNonNullableOwning/InverseSide.php b/tests/Fixture/Entity/EdgeCases/InversedOneToOneWithNonNullableOwning/InverseSide.php index 71537fa6c..f93863fba 100644 --- a/tests/Fixture/Entity/EdgeCases/InversedOneToOneWithNonNullableOwning/InverseSide.php +++ b/tests/Fixture/Entity/EdgeCases/InversedOneToOneWithNonNullableOwning/InverseSide.php @@ -14,21 +14,17 @@ 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_with_non_nullable_owning_inverse_side')] -class InverseSide +#[ORM\Table('inversed_one_to_one_non_nullable_owning_inverse_side')] +class InverseSide extends Base { - #[ORM\Id] - #[ORM\Column] - #[ORM\GeneratedValue(strategy: 'AUTO')] - public ?int $id = null; - public function __construct( - #[ORM\OneToOne(mappedBy: 'inverseSide')] + #[ORM\OneToOne(mappedBy: 'inverseSide')] // @phpstan-ignore doctrine.associationType public OwningSide $owningSide, ) { } diff --git a/tests/Fixture/Entity/EdgeCases/InversedOneToOneWithNonNullableOwning/OwningSide.php b/tests/Fixture/Entity/EdgeCases/InversedOneToOneWithNonNullableOwning/OwningSide.php index 006d09332..a70330983 100644 --- a/tests/Fixture/Entity/EdgeCases/InversedOneToOneWithNonNullableOwning/OwningSide.php +++ b/tests/Fixture/Entity/EdgeCases/InversedOneToOneWithNonNullableOwning/OwningSide.php @@ -14,19 +14,15 @@ 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_with_non_nullable_owning_owning_side')] -class OwningSide +class OwningSide extends Base { - #[ORM\Id] - #[ORM\Column] - #[ORM\GeneratedValue(strategy: 'AUTO')] - public ?int $id = 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/RelationshipWithGlobalEntity/CascadeRelationshipWithGlobalEntity.php b/tests/Fixture/Entity/EdgeCases/InversedOneToOneWithSetter/OwningSide.php similarity index 51% rename from tests/Fixture/Entity/EdgeCases/RelationshipWithGlobalEntity/CascadeRelationshipWithGlobalEntity.php rename to tests/Fixture/Entity/EdgeCases/InversedOneToOneWithSetter/OwningSide.php index 233333f6c..1c8460f78 100644 --- a/tests/Fixture/Entity/EdgeCases/RelationshipWithGlobalEntity/CascadeRelationshipWithGlobalEntity.php +++ b/tests/Fixture/Entity/EdgeCases/InversedOneToOneWithSetter/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\InversedOneToOneWithSetter; 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_setter_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/ManyToOneToSelfReferencing/OwningSide.php b/tests/Fixture/Entity/EdgeCases/ManyToOneToSelfReferencing/OwningSide.php index 14f0c5728..4cc857183 100644 --- a/tests/Fixture/Entity/EdgeCases/ManyToOneToSelfReferencing/OwningSide.php +++ b/tests/Fixture/Entity/EdgeCases/ManyToOneToSelfReferencing/OwningSide.php @@ -27,6 +27,6 @@ class OwningSide #[ORM\GeneratedValue(strategy: 'AUTO')] public ?int $id = null; - #[ORM\ManyToOne(inversedBy: 'owningSide')] + #[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 index 0219f6aa6..3c1dc248c 100644 --- a/tests/Fixture/Entity/EdgeCases/ManyToOneToSelfReferencing/SelfReferencingInverseSide.php +++ b/tests/Fixture/Entity/EdgeCases/ManyToOneToSelfReferencing/SelfReferencingInverseSide.php @@ -13,23 +13,27 @@ 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 +class SelfReferencingInverseSide extends Base { - #[ORM\Id] - #[ORM\Column] - #[ORM\GeneratedValue(strategy: 'AUTO')] - public ?int $id = null; - #[ORM\ManyToOne()] public ?SelfReferencingInverseSide $inverseSide = null; + /** @var Collection */ #[ORM\OneToMany(targetEntity: OwningSide::class, mappedBy: 'inverseSide')] - public ?OwningSide $owningSide = null; + 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/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/EntityInAnotherSchema/Article.php b/tests/Fixture/EntityInAnotherSchema/Article.php index e95aa52b2..62fcf3c3c 100644 --- a/tests/Fixture/EntityInAnotherSchema/Article.php +++ b/tests/Fixture/EntityInAnotherSchema/Article.php @@ -18,7 +18,7 @@ /** * 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 PostgreSQ, as it is the only supported DBMS which handles 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 */ 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/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/CascadeContactFactory.php b/tests/Fixture/Factories/Entity/Contact/ContactFactory.php similarity index 56% rename from tests/Fixture/Factories/Entity/Contact/CascadeContactFactory.php rename to tests/Fixture/Factories/Entity/Contact/ContactFactory.php index e92ac9517..4058ffd23 100644 --- a/tests/Fixture/Factories/Entity/Contact/CascadeContactFactory.php +++ b/tests/Fixture/Factories/Entity/Contact/ContactFactory.php @@ -12,28 +12,28 @@ 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; -use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\Category\CascadeCategoryFactory; +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 */ -final class CascadeContactFactory extends PersistentObjectFactory +class ContactFactory extends PersistentObjectFactory { public static function class(): string { - return CascadeContact::class; + return Contact::class; } - protected function defaults(): array|callable + protected function defaults(): array { return [ 'name' => self::faker()->word(), - 'category' => CascadeCategoryFactory::new(), - 'address' => CascadeAddressFactory::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 79faebd2c..000000000 --- a/tests/Fixture/Factories/Entity/Contact/ProxyCascadeContactFactory.php +++ /dev/null @@ -1,39 +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; -use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\Category\ProxyCascadeCategoryFactory; - -/** - * @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' => ProxyCascadeCategoryFactory::new(), - 'address' => ProxyCascadeAddressFactory::new(), - ]; - } -} diff --git a/tests/Fixture/Factories/Entity/Contact/ProxyContactFactory.php b/tests/Fixture/Factories/Entity/Contact/ProxyContactFactory.php index 5029f235c..1871b8ccb 100644 --- a/tests/Fixture/Factories/Entity/Contact/ProxyContactFactory.php +++ b/tests/Fixture/Factories/Entity/Contact/ProxyContactFactory.php @@ -12,23 +12,23 @@ 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(), diff --git a/tests/Fixture/Factories/Entity/Contact/StandardContactFactory.php b/tests/Fixture/Factories/Entity/Contact/StandardContactFactory.php deleted file mode 100644 index a8ca2d6c7..000000000 --- a/tests/Fixture/Factories/Entity/Contact/StandardContactFactory.php +++ /dev/null @@ -1,39 +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\StandardContact; -use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\Address\StandardAddressFactory; -use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\Category\StandardCategoryFactory; - -/** - * @author Kevin Bond - * - * @extends PersistentObjectFactory - */ -class StandardContactFactory extends PersistentObjectFactory -{ - public static function class(): string - { - return StandardContact::class; - } - - protected function defaults(): array|callable - { - return [ - 'name' => self::faker()->word(), - 'address' => StandardAddressFactory::new(), - 'category' => StandardCategoryFactory::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/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 f142ef0b0..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 @@ -45,7 +45,7 @@ * @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 e84320311..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 $sortBy = 'id') - * @method static StandardCategory|Proxy last(string $sortBy = '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 $sortBy = 'id') - * @phpstan-method static StandardCategory&Proxy last(string $sortBy = '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 670d80df3..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 $sortBy = 'id') - * @method static StandardCategory|Proxy last(string $sortBy = '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 $sortBy = 'id') - * @psalm-method static StandardCategory&Proxy last(string $sortBy = '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/MigrationTests/TestMigrationKernel.php b/tests/Fixture/MigrationTests/TestMigrationKernel.php deleted file mode 100644 index 2038bb906..000000000 --- a/tests/Fixture/MigrationTests/TestMigrationKernel.php +++ /dev/null @@ -1,119 +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\MigrationTests; - -use DAMA\DoctrineTestBundle\DAMADoctrineTestBundle; -use Doctrine\Bundle\DoctrineBundle\DoctrineBundle; -use Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle; -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\ORM\ResetDatabase\ResetDatabaseMode; -use Zenstruck\Foundry\Tests\Fixture\Stories\GlobalInvokableService; -use Zenstruck\Foundry\Tests\Fixture\Stories\GlobalStory; -use Zenstruck\Foundry\ZenstruckFoundryBundle; - -/** - * @author Nicolas PHILIPPE - */ -final class TestMigrationKernel extends Kernel -{ - use MicroKernelTrait; - - public function registerBundles(): iterable - { - yield new FrameworkBundle(); - yield new DoctrineBundle(); - yield new DoctrineMigrationsBundle(); - - yield new ZenstruckFoundryBundle(); - - if (\getenv('USE_DAMA_DOCTRINE_TEST_BUNDLE')) { - yield new DAMADoctrineTestBundle(); - } - } - - protected function configureContainer(ContainerBuilder $c, LoaderInterface $loader): void - { - $c->loadFromExtension('framework', [ - 'http_method_override' => false, - 'secret' => 'S3CRET', - 'router' => ['utf8' => true], - 'test' => true, - ]); - - $c->loadFromExtension('zenstruck_foundry', [ - 'global_state' => [ - GlobalStory::class, - GlobalInvokableService::class, - ], - 'orm' => [ - 'reset' => [ - 'mode' => ResetDatabaseMode::MIGRATE, - 'migrations' => [ - 'configurations' => ($configFile = \getenv('WITH_MIGRATION_CONFIGURATION_FILE')) ? [$configFile] : [], - ], - ], - ], - ]); - - if (!\getenv('WITH_MIGRATION_CONFIGURATION_FILE')) { - $c->loadFromExtension('doctrine_migrations', include __DIR__.'/configs/migration-configuration.php'); - } - - $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('logger', NullLogger::class); - $c->register(GlobalInvokableService::class); - } -} 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/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/MigrationTests/configs/migration-configuration-transactional.php b/tests/Fixture/ResetDatabase/migration-configs/migration-configuration-transactional.php similarity index 72% rename from tests/Fixture/MigrationTests/configs/migration-configuration-transactional.php rename to tests/Fixture/ResetDatabase/migration-configs/migration-configuration-transactional.php index cb0928594..fd4821fc6 100644 --- a/tests/Fixture/MigrationTests/configs/migration-configuration-transactional.php +++ b/tests/Fixture/ResetDatabase/migration-configs/migration-configuration-transactional.php @@ -11,7 +11,7 @@ return [ 'migrations_paths' => [ - 'Zenstruck\\Foundry\\Tests\\Fixture\\MigrationTests\\Migrations' => \dirname(__DIR__).'/Migrations', + 'Zenstruck\\Foundry\\Tests\\Fixture\\ResetDatabase\\Migrations' => \dirname(__DIR__, 4).'/var/cache/Migrations', ], 'transactional' => true, ]; diff --git a/tests/Fixture/MigrationTests/configs/migration-configuration.php b/tests/Fixture/ResetDatabase/migration-configs/migration-configuration.php similarity index 72% rename from tests/Fixture/MigrationTests/configs/migration-configuration.php rename to tests/Fixture/ResetDatabase/migration-configs/migration-configuration.php index def58e6f5..c47f48ae0 100644 --- a/tests/Fixture/MigrationTests/configs/migration-configuration.php +++ b/tests/Fixture/ResetDatabase/migration-configs/migration-configuration.php @@ -11,7 +11,7 @@ return [ 'migrations_paths' => [ - 'Zenstruck\\Foundry\\Tests\\Fixture\\MigrationTests\\Migrations' => \dirname(__DIR__).'/Migrations', + 'Zenstruck\\Foundry\\Tests\\Fixture\\ResetDatabase\\Migrations' => \dirname(__DIR__, 4).'/var/cache/Migrations', ], 'transactional' => false, ]; diff --git a/tests/Fixture/Stories/ServiceStory.php b/tests/Fixture/Stories/ServiceStory.php index 943652f8d..d4b4edf4a 100644 --- a/tests/Fixture/Stories/ServiceStory.php +++ b/tests/Fixture/Stories/ServiceStory.php @@ -11,7 +11,6 @@ namespace Zenstruck\Foundry\Tests\Fixture\Stories; -use Symfony\Component\HttpKernel\KernelInterface; use Symfony\Component\Routing\RouterInterface; use Zenstruck\Foundry\Story; use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\GenericEntityFactory; @@ -19,10 +18,10 @@ /** * @author Nicolas PHILIPPE */ - final class ServiceStory extends Story +final class ServiceStory extends Story { public function __construct( - private readonly RouterInterface $router + private readonly RouterInterface $router, ) { } diff --git a/tests/Fixture/TestKernel.php b/tests/Fixture/TestKernel.php index 182f63e6d..edb4ddfa5 100644 --- a/tests/Fixture/TestKernel.php +++ b/tests/Fixture/TestKernel.php @@ -11,66 +11,31 @@ 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\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\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\Tests\Fixture\Stories\ServiceStory; -use Zenstruck\Foundry\ZenstruckFoundryBundle; /** * @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(); - } - - 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); $c->loadFromExtension('zenstruck_foundry', [ - 'global_state' => [ - GlobalStory::class, - GlobalInvokableService::class, - ], 'orm' => [ 'reset' => [ 'mode' => ResetDatabaseMode::SCHEMA, @@ -78,86 +43,8 @@ protected function configureContainer(ContainerBuilder $c, LoaderInterface $load ], ]); - 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', - ], - - // 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], - ], - ]); - } - - 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); } - - protected function configureRoutes(RoutingConfigurator $routes): void - { - } } diff --git a/tests/Integration/Attribute/WithStory/WithStoryOnClassTest.php b/tests/Integration/Attribute/WithStory/WithStoryOnClassTest.php index 44c94ab81..b64777c46 100644 --- a/tests/Integration/Attribute/WithStory/WithStoryOnClassTest.php +++ b/tests/Integration/Attribute/WithStory/WithStoryOnClassTest.php @@ -28,9 +28,9 @@ /** * @author Nicolas PHILIPPE - * @requires PHPUnit ^11.0 + * @requires PHPUnit >=11.0 */ -#[RequiresPhpunit('^11.0')] +#[RequiresPhpunit('>=11.0')] #[RequiresPhpunitExtension(FoundryExtension::class)] #[WithStory(EntityStory::class)] final class WithStoryOnClassTest extends KernelTestCase diff --git a/tests/Integration/Attribute/WithStory/WithStoryOnMethodTest.php b/tests/Integration/Attribute/WithStory/WithStoryOnMethodTest.php index 2ae15d13e..c6f0df929 100644 --- a/tests/Integration/Attribute/WithStory/WithStoryOnMethodTest.php +++ b/tests/Integration/Attribute/WithStory/WithStoryOnMethodTest.php @@ -29,9 +29,9 @@ /** * @author Nicolas PHILIPPE - * @requires PHPUnit ^11.0 + * @requires PHPUnit >=11.0 */ -#[RequiresPhpunit('^11.0')] +#[RequiresPhpunit('>=11.0')] #[RequiresPhpunitExtension(FoundryExtension::class)] final class WithStoryOnMethodTest extends KernelTestCase { diff --git a/tests/Integration/Attribute/WithStory/WithStoryOnParentClassTest.php b/tests/Integration/Attribute/WithStory/WithStoryOnParentClassTest.php index 59a9a1abb..84c199576 100644 --- a/tests/Integration/Attribute/WithStory/WithStoryOnParentClassTest.php +++ b/tests/Integration/Attribute/WithStory/WithStoryOnParentClassTest.php @@ -23,9 +23,9 @@ /** * @author Nicolas PHILIPPE - * @requires PHPUnit ^11.0 + * @requires PHPUnit >=11.0 */ -#[RequiresPhpunit('^11.0')] +#[RequiresPhpunit('>=11.0')] #[RequiresPhpunitExtension(FoundryExtension::class)] #[WithStory(EntityPoolStory::class)] final class WithStoryOnParentClassTest extends ParentClassWithStoryAttributeTestCase diff --git a/tests/Integration/DataProvider/DataProviderForServiceFactoryInKernelTestCaseTest.php b/tests/Integration/DataProvider/DataProviderForServiceFactoryInKernelTestCaseTest.php index 737b14375..579e34db3 100644 --- a/tests/Integration/DataProvider/DataProviderForServiceFactoryInKernelTestCaseTest.php +++ b/tests/Integration/DataProvider/DataProviderForServiceFactoryInKernelTestCaseTest.php @@ -28,9 +28,9 @@ /** * @author Nicolas PHILIPPE - * @requires PHPUnit ^11.4 + * @requires PHPUnit >=11.4 */ -#[RequiresPhpunit('^11.4')] +#[RequiresPhpunit('>=11.4')] #[RequiresPhpunitExtension(FoundryExtension::class)] final class DataProviderForServiceFactoryInKernelTestCaseTest extends KernelTestCase { diff --git a/tests/Integration/DataProvider/DataProviderInUnitTest.php b/tests/Integration/DataProvider/DataProviderInUnitTest.php index 9f042eee9..e822754c3 100644 --- a/tests/Integration/DataProvider/DataProviderInUnitTest.php +++ b/tests/Integration/DataProvider/DataProviderInUnitTest.php @@ -33,9 +33,9 @@ /** * @author Nicolas PHILIPPE - * @requires PHPUnit ^11.4 + * @requires PHPUnit >=11.4 */ -#[RequiresPhpunit('^11.4')] +#[RequiresPhpunit('>=11.4')] #[RequiresPhpunitExtension(FoundryExtension::class)] final class DataProviderInUnitTest extends TestCase { diff --git a/tests/Integration/DataProvider/DataProviderWithNonProxyFactoryInKernelTestCaseTest.php b/tests/Integration/DataProvider/DataProviderWithNonProxyFactoryInKernelTestCaseTest.php index 7b6544811..b948ec08e 100644 --- a/tests/Integration/DataProvider/DataProviderWithNonProxyFactoryInKernelTestCaseTest.php +++ b/tests/Integration/DataProvider/DataProviderWithNonProxyFactoryInKernelTestCaseTest.php @@ -25,9 +25,9 @@ /** * @author Nicolas PHILIPPE - * @requires PHPUnit ^11.4 + * @requires PHPUnit >=11.4 */ -#[RequiresPhpunit('^11.4')] +#[RequiresPhpunit('>=11.4')] #[RequiresPhpunitExtension(FoundryExtension::class)] final class DataProviderWithNonProxyFactoryInKernelTestCaseTest extends KernelTestCase { diff --git a/tests/Integration/DataProvider/DataProviderWithProxyFactoryInKernelTestCase.php b/tests/Integration/DataProvider/DataProviderWithProxyFactoryInKernelTestCase.php index 79da54529..c48ebc4ff 100644 --- a/tests/Integration/DataProvider/DataProviderWithProxyFactoryInKernelTestCase.php +++ b/tests/Integration/DataProvider/DataProviderWithProxyFactoryInKernelTestCase.php @@ -29,9 +29,9 @@ /** * @author Nicolas PHILIPPE - * @requires PHPUnit ^11.4 + * @requires PHPUnit >=11.4 */ -#[RequiresPhpunit('^11.4')] +#[RequiresPhpunit('>=11.4')] #[RequiresPhpunitExtension(FoundryExtension::class)] abstract class DataProviderWithProxyFactoryInKernelTestCase extends KernelTestCase { @@ -46,7 +46,6 @@ public function assert_it_can_create_one_object_in_data_provider(?GenericModel $ self::assertInstanceOf(Proxy::class, $providedData); self::assertNotInstanceOf(Proxy::class, unproxy($providedData)); // asserts two proxies are not nested - self::assertInstanceOf(GenericModel::class, $providedData); self::assertSame('value set in data provider', $providedData->getProp1()); } diff --git a/tests/Integration/DataProvider/GenericDocumentProxyFactoryTest.php b/tests/Integration/DataProvider/GenericDocumentProxyFactoryTest.php index c163c1df5..058892c29 100644 --- a/tests/Integration/DataProvider/GenericDocumentProxyFactoryTest.php +++ b/tests/Integration/DataProvider/GenericDocumentProxyFactoryTest.php @@ -19,9 +19,9 @@ /** * @author Nicolas PHILIPPE - * @requires PHPUnit ^11.4 + * @requires PHPUnit >=11.4 */ -#[RequiresPhpunit('^11.4')] +#[RequiresPhpunit('>=11.4')] #[RequiresPhpunitExtension(FoundryExtension::class)] final class GenericDocumentProxyFactoryTest extends DataProviderWithProxyFactoryInKernelTestCase { diff --git a/tests/Integration/DataProvider/GenericEntityProxyFactoryTest.php b/tests/Integration/DataProvider/GenericEntityProxyFactoryTest.php index d34064c86..1658df558 100644 --- a/tests/Integration/DataProvider/GenericEntityProxyFactoryTest.php +++ b/tests/Integration/DataProvider/GenericEntityProxyFactoryTest.php @@ -19,9 +19,9 @@ /** * @author Nicolas PHILIPPE - * @requires PHPUnit ^11.4 + * @requires PHPUnit >=11.4 */ -#[RequiresPhpunit('^11.4')] +#[RequiresPhpunit('>=11.4')] #[RequiresPhpunitExtension(FoundryExtension::class)] final class GenericEntityProxyFactoryTest extends DataProviderWithProxyFactoryInKernelTestCase { 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..ba73e8a73 --- /dev/null +++ b/tests/Integration/ForceFactoriesTraitUsage/KernelTestCaseWithoutFactoriesTraitTestCase.php @@ -0,0 +1,91 @@ + + * + * 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->expectUserDeprecationMessageMatches('/In order to use Foundry, you must use the trait/'); + + Object1Factory::createOne(); + } + + #[Test] + #[IgnoreDeprecations] + public function using_foundry_without_trait_should_throw_even_when_kernel_is_booted(): void + { + $this->expectUserDeprecationMessageMatches('/In order to use Foundry, you must use the trait/'); + + 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->expectUserDeprecationMessageMatches('/In order to use Foundry, you must use the trait/'); + + 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(); + } +} 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/Maker/MakeFactoryTest.php b/tests/Integration/Maker/MakeFactoryTest.php index 4e4b741eb..5d8af22d8 100644 --- a/tests/Integration/Maker/MakeFactoryTest.php +++ b/tests/Integration/Maker/MakeFactoryTest.php @@ -17,8 +17,8 @@ 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; @@ -66,13 +66,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 +87,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 +113,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 +132,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')); } /** diff --git a/tests/Integration/Migration/ResetDatabaseWithMigrationTest.php b/tests/Integration/Migration/ResetDatabaseWithMigrationTest.php deleted file mode 100644 index c4e5cfc02..000000000 --- a/tests/Integration/Migration/ResetDatabaseWithMigrationTest.php +++ /dev/null @@ -1,106 +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\Migration; - -use Symfony\Bundle\FrameworkBundle\Console\Application; -use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; -use Symfony\Component\Console\Input\ArrayInput; -use Symfony\Component\Console\Output\BufferedOutput; -use Zenstruck\Foundry\Test\Factories; -use Zenstruck\Foundry\Test\ResetDatabase; -use Zenstruck\Foundry\Tests\Fixture\Entity\GlobalEntity; -use Zenstruck\Foundry\Tests\Fixture\EntityInAnotherSchema\Article; -use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\Contact\StandardContactFactory; -use Zenstruck\Foundry\Tests\Fixture\MigrationTests\TestMigrationKernel; -use Zenstruck\Foundry\Tests\Integration\RequiresORM; - -use function Zenstruck\Foundry\Persistence\persist; -use function Zenstruck\Foundry\Persistence\repository; - -/** - * @author Nicolas PHILIPPE - */ -final class ResetDatabaseWithMigrationTest extends KernelTestCase -{ - use Factories; - use RequiresORM; - use ResetDatabase; - - /** - * @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() - ); - - // 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); - } - - /** - * @test - */ - public function it_can_store_object(): void - { - StandardContactFactory::assert()->count(0); - - StandardContactFactory::createOne(); - - StandardContactFactory::assert()->count(1); - } - - /** - * @test - * @depends it_can_store_object - */ - public function it_starts_from_fresh_db(): void - { - StandardContactFactory::assert()->count(0); - } - - /** - * @test - */ - public function global_objects_are_created(): void - { - repository(GlobalEntity::class)->assert()->count(2); - } - - /** - * @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); - } - - protected static function getKernelClass(): string - { - return TestMigrationKernel::class; - } -} 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 a0cc97804..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 = self::contactFactory() - ->afterInstantiate(function() { - self::categoryFactory()::repository()->assert()->empty(); - self::addressFactory()::repository()->assert()->empty(); - self::tagFactory()::repository()->assert()->empty(); - }) - ->create([ - 'tags' => self::tagFactory()->many(3), - 'category' => self::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 = self::tagFactory() - ->afterInstantiate(function() { - self::categoryFactory()::repository()->assert()->empty(); - self::addressFactory()::repository()->assert()->empty(); - self::contactFactory()::repository()->assert()->empty(); - }) - ->create([ - 'contacts' => self::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 = self::categoryFactory() - ->afterInstantiate(function() { - self::contactFactory()::repository()->assert()->empty(); - self::addressFactory()::repository()->assert()->empty(); - self::tagFactory()::repository()->assert()->empty(); - }) - ->create([ - 'contacts' => self::contactFactory()->many(3), - ]) - ; - - $this->assertCount(3, $category->getContacts()); - - foreach ($category->getContacts() as $contact) { - $this->assertNotNull($contact->id); - } - } - - protected static function contactFactory(): PersistentObjectFactory - { - return CascadeContactFactory::new(); // @phpstan-ignore return.type - } - - protected static function categoryFactory(): PersistentObjectFactory - { - return CascadeCategoryFactory::new(); // @phpstan-ignore return.type - } - - protected static function tagFactory(): PersistentObjectFactory - { - return CascadeTagFactory::new(); // @phpstan-ignore return.type - } - - protected static 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 9476b3ee3..27a94e13c 100644 --- a/tests/Integration/ORM/EdgeCasesRelationshipTest.php +++ b/tests/Integration/ORM/EdgeCasesRelationshipTest.php @@ -13,72 +13,41 @@ 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\DoctrineCascadeRelationship\ChangesEntityRelationshipCascadePersist; +use Zenstruck\Foundry\Tests\Fixture\DoctrineCascadeRelationship\UsingRelationships; 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\RelationshipWithGlobalEntity; 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; - - /** - * @test - * @param PersistentObjectFactory $relationshipWithGlobalEntityFactory - * @dataProvider relationshipWithGlobalEntityFactoryProvider - */ - public function it_can_use_flush_after_and_entity_from_global_state(PersistentObjectFactory $relationshipWithGlobalEntityFactory, bool $asProxy): void - { - $globalEntitiesCount = persistent_factory(GlobalEntity::class)::repository()->count(); - - flush_after(function() use ($relationshipWithGlobalEntityFactory, $asProxy) { - $globalEntity = $asProxy ? GlobalStory::globalEntityProxy() : GlobalStory::globalEntity(); - self::assertSame($asProxy, $globalEntity instanceof Proxy); - - $relationshipWithGlobalEntityFactory->create(['globalEntity' => $globalEntity]); - }); - - // assert no extra GlobalEntity have been created - persistent_factory(GlobalEntity::class)::assert()->count($globalEntitiesCount); - - $relationshipWithGlobalEntityFactory::assert()->count(1); - - $entity = $relationshipWithGlobalEntityFactory::repository()->first(); - self::assertSame(GlobalStory::globalEntity(), $entity?->getGlobalEntity()); - } - - public static function relationshipWithGlobalEntityFactoryProvider(): iterable + use ChangesEntityRelationshipCascadePersist, Factories, RequiresORM, ResetDatabase; + + /** @test */ + #[Test] + #[DataProvider('provideCascadeRelationshipsCombinations')] + #[UsingRelationships(RichDomainMandatoryRelationship\OwningSide::class, ['main'])] + #[RequiresPhpunit('>=11.4')] + public function inversed_relationship_mandatory(): 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]; - } + $owningSideEntityFactory = persistent_factory(RichDomainMandatoryRelationship\OwningSide::class); + $inversedSideEntityFactory = persistent_factory(RichDomainMandatoryRelationship\InversedSide::class); - /** - * @test - * @param PersistentObjectFactory $inversedSideEntityFactory - * @param PersistentObjectFactory $owningSideEntityFactory - * @dataProvider richDomainMandatoryRelationshipFactoryProvider - */ - public function inversed_relationship_mandatory(PersistentObjectFactory $inversedSideEntityFactory, PersistentObjectFactory $owningSideEntityFactory): void - { $inversedSideEntity = $inversedSideEntityFactory->create([ 'relations' => $owningSideEntityFactory->many(2), ]); @@ -88,29 +57,11 @@ public function inversed_relationship_mandatory(PersistentObjectFactory $inverse $inversedSideEntityFactory::assert()->count(1); } - public static function richDomainMandatoryRelationshipFactoryProvider(): iterable - { - 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), - ]; - } - - /** - * @test - */ + /** @test */ + #[Test] + #[DataProvider('provideCascadeRelationshipsCombinations')] + #[UsingRelationships(InversedOneToOneWithNonNullableOwning\OwningSide::class, ['inverseSide'])] + #[RequiresPhpunit('>=11.4')] public function inverse_one_to_one_with_non_nullable_inverse_side(): void { $owningSideFactory = persistent_factory(InversedOneToOneWithNonNullableOwning\OwningSide::class); @@ -124,6 +75,52 @@ public function inverse_one_to_one_with_non_nullable_inverse_side(): void self::assertSame($inverseSide, $inverseSide->owningSide->inverseSide); } + /** @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); + + $inverseSide = $inverseSideFactory->create(['owningSide' => $owningSideFactory]); + + $owningSideFactory::assert()->count(1); + $inverseSideFactory::assert()->count(1); + + self::assertSame($inverseSide, $inverseSide->getOwningSide()?->inverseSide); + } + + /** @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 + { + $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 */ diff --git a/tests/Integration/ORM/EntityFactoryRelationshipTestCase.php b/tests/Integration/ORM/EntityFactoryRelationshipTestCase.php deleted file mode 100644 index 2e7c05e87..000000000 --- a/tests/Integration/ORM/EntityFactoryRelationshipTestCase.php +++ /dev/null @@ -1,355 +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\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\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 = static::contactFactory()::createOne([ - 'category' => static::categoryFactory(), - ]); - - static::contactFactory()::repository()->assert()->count(1); - static::categoryFactory()::repository()->assert()->count(1); - - $this->assertNotNull($contact->id); - $this->assertNotNull($contact->getCategory()?->id); - } - - /** - * @test - */ - public function disabling_persistence_cascades_to_children(): void - { - $contact = static::contactFactory()->withoutPersisting()->create([ - 'tags' => static::tagFactory()->many(3), - 'category' => static::categoryFactory(), - ]); - - static::contactFactory()::repository()->assert()->empty(); - static::categoryFactory()::repository()->assert()->empty(); - static::tagFactory()::repository()->assert()->empty(); - static::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 = static::categoryFactory()->withoutPersisting()->create([ - 'contacts' => static::contactFactory()->many(3), - ]); - - static::contactFactory()::repository()->assert()->empty(); - static::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 - * @param FactoryCollection|list> $contacts - * @dataProvider one_to_many_provider - */ - public function one_to_many(FactoryCollection|array $contacts): void - { - $category = static::categoryFactory()::createOne([ - 'contacts' => $contacts, - ]); - - static::contactFactory()::repository()->assert()->count(2); - static::categoryFactory()::repository()->assert()->count(1); - $this->assertNotNull($category->id); - $this->assertCount(2, $category->getContacts()); - - foreach ($category->getContacts() as $contact) { - $this->assertSame($category->id, $contact->getCategory()?->id); - } - } - - public static function one_to_many_provider(): iterable - { - yield 'as a factory collection' => [static::contactFactory()->many(2)]; - yield 'as an array of factories' => [[static::contactFactory(), static::contactFactory()]]; - } - - /** - * @test - */ - public function inverse_one_to_many_relationship(): void - { - static::categoryFactory()::assert()->count(0); - static::contactFactory()::assert()->count(0); - - $category = static::categoryFactory()->create([ - 'contacts' => [ - static::contactFactory()->with(['category' => null]), - static::contactFactory()->create(['category' => null]), - ], - ]); - - static::categoryFactory()::assert()->count(1); - static::contactFactory()::assert()->count(2); - - foreach ($category->getContacts() as $contact) { - $this->assertSame($category->id, $contact->getCategory()?->id); - } - } - - /** - * @test - */ - public function many_to_many_owning(): void - { - $tag = static::tagFactory()::createOne([ - 'contacts' => static::contactFactory()->many(3), - ]); - - static::contactFactory()::repository()->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); - } - } - - /** - * @test - */ - public function many_to_many_owning_as_array(): void - { - $tag = static::tagFactory()::createOne([ - 'contacts' => [static::contactFactory(), static::contactFactory(), static::contactFactory()], - ]); - - static::contactFactory()::repository()->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); - } - } - - /** - * @test - */ - public function many_to_many_inverse(): void - { - $contact = static::contactFactory()::createOne([ - 'tags' => static::tagFactory()->many(3), - ]); - - static::contactFactory()::repository()->assert()->count(1); - static::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 = static::contactFactory()::createOne(); - - static::contactFactory()::repository()->assert()->count(1); - static::addressFactory()::repository()->assert()->count(1); - - $this->assertNotNull($contact->id); - $this->assertNotNull($contact->getAddress()->id); - } - - /** - * @test - */ - 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 - */ - public function one_to_many_with_two_relationships_same_entity(): void - { - $category = static::categoryFactory()->create([ - 'contacts' => static::contactFactory()->many(2), - 'secondaryContacts' => static::contactFactory() - ->with(['category' => null]) // ensure no "main category" is set for secondary contacts - ->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 - */ - public function one_to_many_with_two_relationships_same_entity_and_adders(): void - { - $category = static::categoryFactory()->create([ - 'addContact' => static::contactFactory()->with(['category' => null]), - 'addSecondaryContact' => static::contactFactory()->with(['category' => null]), - ]); - - $this->assertCount(1, $category->getContacts()); - $this->assertCount(1, $category->getSecondaryContacts()); - static::contactFactory()::assert()->count(2); - static::categoryFactory()::assert()->count(1); - } - - /** - * @test - */ - 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(3), - ]); - - $this->assertCount(3, $tag->getContacts()); - $this->assertCount(3, $tag->getSecondaryContacts()); - static::tagFactory()::assert()->count(1); - static::contactFactory()::assert()->count(6); - } - - /** - * @test - */ - public function inversed_one_to_one(): void - { - $addressFactory = $this->addressFactory(); - $contactFactory = $this->contactFactory(); - - $address = $addressFactory->create(['contact' => $contactFactory]); - - self::assertNotNull($address->getContact()); - - $addressFactory::assert()->count(1); - $contactFactory::assert()->count(1); - } - - /** - * @test - */ - 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 - */ - 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); - } - - /** - * @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; -} 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/ProxyEntityFactoryRelationshipTestCase.php b/tests/Integration/ORM/EntityRelationship/ProxyEntityFactoryRelationshipTest.php similarity index 62% rename from tests/Integration/ORM/ProxyEntityFactoryRelationshipTestCase.php rename to tests/Integration/ORM/EntityRelationship/ProxyEntityFactoryRelationshipTest.php index d9217bbe8..ebdaca361 100644 --- a/tests/Integration/ORM/ProxyEntityFactoryRelationshipTestCase.php +++ b/tests/Integration/ORM/EntityRelationship/ProxyEntityFactoryRelationshipTest.php @@ -1,5 +1,7 @@ * @author Nicolas PHILIPPE - * - * @method PersistentProxyObjectFactory contactFactory() - * @method PersistentProxyObjectFactory categoryFactory() - * @method PersistentProxyObjectFactory tagFactory() - * @method PersistentProxyObjectFactory
addressFactory() + * @requires PHPUnit >=11.4 */ -abstract class ProxyEntityFactoryRelationshipTestCase extends EntityFactoryRelationshipTestCase +#[RequiresPhpunit('>=11.4')] +final class ProxyEntityFactoryRelationshipTest extends EntityFactoryRelationshipTestCase { - /** - * @see https://github.com/zenstruck/foundry/issues/42 - * - * @test - */ + /** @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.nonObject + 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(); @@ -57,9 +59,10 @@ public function doctrine_proxies_are_converted_to_foundry_proxies(): void $this->assertInstanceOf(static::categoryFactory()::class(), $category); } - /** - * @test - */ + /** @test */ + #[Test] + #[DataProvider('provideCascadeRelationshipsCombinations')] + #[UsingRelationships(Contact::class, ['category'])] public function it_can_add_proxy_to_many_to_one(): void { $contact = static::contactFactory()->create(); @@ -71,9 +74,10 @@ public function it_can_add_proxy_to_many_to_one(): void static::contactFactory()::assert()->exists(['category' => $category]); } - /** - * @test - */ + /** @test */ + #[Test] + #[DataProvider('provideCascadeRelationshipsCombinations')] + #[UsingRelationships(Contact::class, ['tags'])] public function it_can_add_proxy_to_one_to_many(): void { $contact = static::contactFactory()->create(); @@ -86,9 +90,8 @@ public function it_can_add_proxy_to_one_to_many(): void self::assertContains($contact->_real(), $tag->getContacts()); } - /** - * @test - */ + /** @test */ + #[Test] public function can_assert_persisted(): void { static::contactFactory()->create()->_assertPersisted(); @@ -98,9 +101,8 @@ public function can_assert_persisted(): void ; } - /** - * @test - */ + /** @test */ + #[Test] public function can_assert_not_persisted(): void { static::contactFactory()->withoutPersisting()->create()->_assertNotPersisted(); @@ -110,9 +112,8 @@ public function can_assert_not_persisted(): void ; } - /** - * @test - */ + /** @test */ + #[Test] public function can_remove_and_assert_not_persisted(): void { static::contactFactory() @@ -123,15 +124,33 @@ public function can_remove_and_assert_not_persisted(): void ; } - /** - * @test - */ - public function cannot_use_assert_persisted_when_entity_has_changes(): void + /** @test */ + #[Test] + public function can_use_assert_persisted_when_entity_has_changes(): void { $contact = static::contactFactory()->create(); $contact->setName('foo'); - $this->expectException(RefreshObjectFailed::class); $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/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 46b54c83b..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 static 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 3c5a401fe..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 static function contactFactory(): PersistentObjectFactory - { - return ProxyCascadeContactFactory::new(); // @phpstan-ignore return.type - } - - protected static function categoryFactory(): PersistentObjectFactory - { - return ProxyCascadeCategoryFactory::new(); // @phpstan-ignore return.type - } - - protected static function tagFactory(): PersistentObjectFactory - { - return ProxyCascadeTagFactory::new(); // @phpstan-ignore return.type - } - - protected static 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 064d58b9f..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 static function contactFactory(): PersistentObjectFactory - { - return ProxyContactFactory::new(); // @phpstan-ignore return.type - } - - protected static function categoryFactory(): PersistentObjectFactory - { - return ProxyCategoryFactory::new(); // @phpstan-ignore return.type - } - - protected static function tagFactory(): PersistentObjectFactory - { - return ProxyTagFactory::new(); // @phpstan-ignore return.type - } - - protected static function addressFactory(): PersistentObjectFactory - { - return ProxyAddressFactory::new(); // @phpstan-ignore return.type - } -} 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 acd5b57ae..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 static function contactFactory(): PersistentObjectFactory - { - return StandardContactFactory::new(); // @phpstan-ignore return.type - } - - protected static function categoryFactory(): PersistentObjectFactory - { - return StandardCategoryFactory::new(); // @phpstan-ignore return.type - } - - protected static function tagFactory(): PersistentObjectFactory - { - return StandardTagFactory::new(); // @phpstan-ignore return.type - } - - protected static 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/GenericProxyFactoryTestCase.php b/tests/Integration/Persistence/GenericProxyFactoryTestCase.php index c8af22efb..014ed2a0f 100644 --- a/tests/Integration/Persistence/GenericProxyFactoryTestCase.php +++ b/tests/Integration/Persistence/GenericProxyFactoryTestCase.php @@ -282,6 +282,27 @@ 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 */ 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-migrate.php b/tests/bootstrap-migrate.php deleted file mode 100644 index 79e2e7b7c..000000000 --- a/tests/bootstrap-migrate.php +++ /dev/null @@ -1,45 +0,0 @@ - - * - * 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\MigrationTests\TestMigrationKernel; - -use function Zenstruck\Foundry\application; -use function Zenstruck\Foundry\runCommand; - -require \dirname(__DIR__).'/vendor/autoload.php'; - -$fs = new Filesystem(); - -$fs->remove(__DIR__.'/../var'); - -(new Dotenv())->usePutenv()->loadEnv(__DIR__.'/../.env'); - -$fs->remove(__DIR__.'/Fixture/MigrationTests/Migrations'); -$fs->mkdir(__DIR__.'/Fixture/MigrationTests/Migrations'); - -$kernel = new TestMigrationKernel('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('WITH_MIGRATION_CONFIGURATION_FILE')) { - $configuration = '--configuration '.\getcwd().'/'.\getenv('WITH_MIGRATION_CONFIGURATION_FILE'); -} -runCommand($application, "doctrine:migrations:diff {$configuration}"); -runCommand($application, 'doctrine:database:drop --force', canFail: true); - -$kernel->shutdown(); 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 a1b7ae6c9..c8aa9d218 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -12,10 +12,10 @@ use Symfony\Component\Dotenv\Dotenv; use Symfony\Component\Filesystem\Filesystem; -require \dirname(__DIR__) . '/vendor/autoload.php'; +require \dirname(__DIR__).'/vendor/autoload.php'; $fs = new Filesystem(); -$fs->remove(__DIR__ . '/../var'); +$fs->remove(__DIR__.'/../var/cache'); -(new Dotenv())->usePutenv()->loadEnv(__DIR__ . '/../.env'); +(new Dotenv())->usePutenv()->loadEnv(__DIR__.'/../.env');