From 7cd3b59481db82d5033427ba0f7f7f729c7beecb Mon Sep 17 00:00:00 2001 From: Kevin Bond Date: Wed, 23 Oct 2024 13:41:01 -0400 Subject: [PATCH 001/102] changelog: update [skip ci] --- CHANGELOG.md | 777 ++++++++------------------------------------------- 1 file changed, 119 insertions(+), 658 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e3aa0ddb9..7775670bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,29 @@ # CHANGELOG +## [v2.1.0](https://github.com/zenstruck/foundry/releases/tag/v2.1.0) + +October 3rd, 2024 - [v2.0.9...v2.1.0](https://github.com/zenstruck/foundry/compare/v2.0.9...v2.1.0) + +* 0f72ea5 fix: allow non object state in stories (#699) by @Brewal +* 6482357 feat: allow to configure migrations configuration files (#686) by @MatTheCat + +## [v2.0.9](https://github.com/zenstruck/foundry/releases/tag/v2.0.9) + +September 2nd, 2024 - [v2.0.8...v2.0.9](https://github.com/zenstruck/foundry/compare/v2.0.8...v2.0.9) + +* b0a5d3d Fix Psalm TooManyTemplateParams (#693) by @ddeboer + +## [v2.0.8](https://github.com/zenstruck/foundry/releases/tag/v2.0.8) + +August 29th, 2024 - [v2.0.7...v2.0.8](https://github.com/zenstruck/foundry/compare/v2.0.7...v2.0.8) + +* 3eebbf9 Have `flush_after()` return the callback's return (#691) by @HypeMC +* 33d5870 doc: Fix range call instead of many (#688) by @ternel +* 33595b9 chore: add a wrapper for PHPUnit binary (#683) by @nikophil +* 8bf8c4c docs: Fix CategoryStory codeblock (#681) by @smnandre +* f89d43e doc: Minor fixes (#679) by @smnandre +* 65c1cc2 fix: add phpdoc to improve proxy factories autocompletion (#675) by @nikophil + ## [v2.0.7](https://github.com/zenstruck/foundry/releases/tag/v2.0.7) July 12th, 2024 - [v2.0.6...v2.0.7](https://github.com/zenstruck/foundry/compare/v2.0.6...v2.0.7) @@ -8,27 +32,32 @@ July 12th, 2024 - [v2.0.6...v2.0.7](https://github.com/zenstruck/foundry/compare * 49f5e1d Fix faker php urls (#671) by @BackEndTea * 7719b0d chore(CI): Enable documentation linter (#657) by @cezarpopa -## [v2.0.2](https://github.com/zenstruck/foundry/releases/tag/v2.0.2) - -June 14th, 2024 - [v2.0.1...v2.0.2](https://github.com/zenstruck/foundry/compare/v2.0.1...v2.0.2) - +## [v2.0.6](https://github.com/zenstruck/foundry/releases/tag/v2.0.6) + +July 4th, 2024 - [v1.38.3...v2.0.6](https://github.com/zenstruck/foundry/compare/v1.38.3...v2.0.6) + +* 52ca7b7 fix: only restore error handler for PHPUnit 10 or superior (#668) by @nikophil +* b5090aa docs: Fix broken link to Without Persisting (#660) by @simoheinonen +* 35b0404 feat: re-add Proxy assertions (#663) by @nikophil +* 6105a36 fix: make proxy work with last symfony/var-exporter version (#664) by @nikophil +* e8623a3 [DOC] Fix Upgrade Guide URL Rendering (#654) by @cezarpopa +* f7f133a fix: create ArrayCollection if needed (#645) by @nikophil +* 779bee4 fix: after_flush() can use objects created in global state (#653) by @nikophil +* 72e48bf tests(ci): add test permutation for PHPUnit >= 10 (#647) by @nikophil +* 1edf948 docs: fix incoherence (#652) by @nikophil +* 1c66e39 minor: improve repository assertion messages (#651) by @nikophil +* 0989c5d fix: don't try to proxify objects that are not persistable (#646) by @nikophil +* 50ae3dc fix: handle contravariance problem when proxifying class with unserialize method (#644) by @nikophil +* 6f0835f fix(2.x): only reset error handler in before class hook (#643) by @nikophil +* 3c31193 test: add test with multiple ORM schemas (#629) by @vincentchalamon +* 303211a fix: unproxy args in proxy objects (#635) by @nikophil * b76c294 fix(2.x): support Symfony 7.1 (#622) by @nikophil * 9cd97b7 docs: Improve DX for tests (#636) by @matthieumota * 17b0228 fix(2.x): add back second parameter for after persist callbacks (#631) by @nikophil * 0c7b3af docs: Fix typo in the upgrade guide (#624) by @stof * 933ebbd docs: upgrade readme with a link to upgrade guide (#620) by @nikophil - -## [v2.0.1](https://github.com/zenstruck/foundry/releases/tag/v2.0.1) - -June 10th, 2024 - [v2.0.0...v2.0.1](https://github.com/zenstruck/foundry/compare/v2.0.0...v2.0.1) - * 5f0ce76 Fix `Instantiator::allowExtra` example (#616) by @norkunas * c2cbcbc fix(orm): reset database instead of dropping the schema when using migrations (#615) by @vincentchalamon - -## [v2.0.0](https://github.com/zenstruck/foundry/releases/tag/v2.0.0) - -June 7th, 2024 - [v1.38.0...v2.0.0](https://github.com/zenstruck/foundry/compare/v1.38.0...v2.0.0) - * 1393c13 docs: update docs for foundry v2 (#613) by @nikophil * 0034c78 feat(2.x, make:facotry): add phpdoc conditionnally with --with-phpdoc option (#611) by @nikophil * a3ec473 fix: prevent refresh error with autorefresh (#610) by @nikophil @@ -49,664 +78,96 @@ June 7th, 2024 - [v1.38.0...v2.0.0](https://github.com/zenstruck/foundry/compare * 53f25a2 rector: rewrite phpdoc (#571) by @nikophil * ecbc615 refactor: Foundry 2 BC layer (#515) by @nikophil -## [v1.38.0](https://github.com/zenstruck/foundry/releases/tag/v1.38.0) - -June 7th, 2024 - [v1.37.0...v1.38.0](https://github.com/zenstruck/foundry/compare/v1.37.0...v1.38.0) - -* b3adc86 docs(v1): improve upgrade guide (#614) by @nikophil -* 691741f doc: update (#594) by @kbond, @nikophil -* 431afa3 minor(make:factory): default false for `embedded` and `targetDocument` mappings (#602) by @melkamar -* 6102488 fix(1.x): support entities with readonly properties (#598) by @nikophil -* 5af3f0f feat: Configure default_namespace for make_story (#592) by @dmitryuk, a.dmitryuk -* 00e6b86 fix(rector): misc fixes in rector rules (#586) by @nikophil -* afad017 fix: can use ObjectFactories as service (#585) by @nikophil -* b66ed4c fix: restore factory collection type (#584) by @nikophil -* 7cfab99 fix: restore error handler in Foundry's traits (#577) by @nikophil -* d55c728 fix(sca): fix type system with legacy FactoryCollection (#583) by @nikophil -* d38568f docs: typo fixes (#578) by @jrushlow -* bbcef6a refactor: add a BC layer for 2.x (#515) by @nikophil - -## [v1.37.0](https://github.com/zenstruck/foundry/releases/tag/v1.37.0) - -March 20th, 2024 - [v1.36.1...v1.37.0](https://github.com/zenstruck/foundry/compare/v1.36.1...v1.37.0) - -* 4a9b00e feat: support `doctrine/orm` 3 (#569) by @nikophil -* 68148e1 use phpunit attributes (#562) by @jrushlow -* f803868 fix: only run maker test on php > 8.0 (#570) by @nikophil -* 669bc2d chore: drop SF 6.3 from matrix (#558) by @kbond -* cf87b97 minor: add sqlite to CI (#557) by @kbond - -## [v1.36.1](https://github.com/zenstruck/foundry/releases/tag/v1.36.1) - -December 14th, 2023 - [v1.36.0...v1.36.1](https://github.com/zenstruck/foundry/compare/v1.36.0...v1.36.1) - -* 8ea43d5 minor: add Symfony 7.0 to the test matrix (#539) by @kbond -* 4c4d099 minor: update psalm (#539) by @kbond -* c9ddf74 minor: update/fix phpstan issues (#539) by @kbond -* de3b26e minor: allow `dama/doctrine-test-bundle` 8.x (#539) by @kbond -* a1a22ce doc: fix memoize example (#526) by @kbond -* b7c0cb7 minor(ci): remove mysql/no-dama from matrix (#521) by @kbond -* e41bbe8 doc: add warning about complex global state performance (#520) by @kbond -* 81eb3b8 doc: add section on `paratestphp/paratest` (#520) by @kbond -* 8666777 doc: adjust test performance section (#520) by @kbond -* a0bbf5b fix: phpunit xsd path (#520) by @kbond -* ed0cd7b chore: remove Makefile (#513) by @nikophil - -## [v1.36.0](https://github.com/zenstruck/foundry/releases/tag/v1.36.0) - -October 13th, 2023 - [v1.35.0...v1.36.0](https://github.com/zenstruck/foundry/compare/v1.35.0...v1.36.0) - -* 5ffa1cd minor: allow Symfony 7.0 (#509) by @kbond -* 925eab3 fix: proxy should not try to refresh when persist is disabled (#508) by @nikophil -* e8b3ff5 feat: allow stories to call their own pool (#506) by @nikophil -* 881280e docs: Move note about immutability into States section (#504) by @pavelmaca -* d6eb810 doc: fix typo (#502) by @ternel -* 131517a minor: flag repo as dev-dependency for composer (#501) by @Chris53897, @Chris8934 -* 4da5628 fix: replace doctrine:query:sql with dbal:run-sql (#492) by @KDederichs - -## [v1.35.0](https://github.com/zenstruck/foundry/releases/tag/v1.35.0) - -August 10th, 2023 - [v1.34.1...v1.35.0](https://github.com/zenstruck/foundry/compare/v1.34.1...v1.35.0) - -* 342a2c9 feat: disable persist globally (#488) by @nikophil -* fbccdd3 fix: do not flush global state twice when dama is not enabled (#489) by @nikophil - -## [v1.34.1](https://github.com/zenstruck/foundry/releases/tag/v1.34.1) - -August 4th, 2023 - [v1.34.0...v1.34.1](https://github.com/zenstruck/foundry/compare/v1.34.0...v1.34.1) - -* 2433482 minor: set `Factory::isPersisting()` protected (#486) by @nikophil -* b86ca0a fix: deprecated code in `StubCommand` (#485) by @kbond - -## [v1.34.0](https://github.com/zenstruck/foundry/releases/tag/v1.34.0) - -July 12th, 2023 - [v1.33.0...v1.34.0](https://github.com/zenstruck/foundry/compare/v1.33.0...v1.34.0) - -* 850858e feat(lazy): Add memoized LazyValue (#475) by @ndench, @kbond -* 6d2c7ce fix: maker with cs fixer (#476) by @nikophil -* b6b6501 Fix a missing parenthesis in docs (#470) by @jmsche - -## [v1.33.0](https://github.com/zenstruck/foundry/releases/tag/v1.33.0) - -May 23rd, 2023 - [v1.32.0...v1.33.0](https://github.com/zenstruck/foundry/compare/v1.32.0...v1.33.0) - -* 98eca98 tests: can create object with fields name different from construct (#466) by @nikophil -* b48399d feat: drop all connections before dropping db for postgres (#458) by @nikophil -* c2719d1 dependencies: bump faker to 1.10 (#464) by @nikophil -* a5ed31c feat: allow setters as factory attribute (#457) by @nikophil - -## [v1.32.0](https://github.com/zenstruck/foundry/releases/tag/v1.32.0) - -May 11th, 2023 - [v1.31.0...v1.32.0](https://github.com/zenstruck/foundry/compare/v1.31.0...v1.32.0) - -* 27b4d7f feat: allow ModelFactory::findOrCreate() in unit tests (#461) by @nikophil -* 68c6a5e ci: add Symfony 6.3 to test matrix (#459) by @kbond -* 912f134 fix: makefile minor fixes (#451) by @nikophil -* d8fafed minor: improve makefile (#449) by @nikophil -* 9cce6dc docs: add static ide hint to stories (#448) by @adrianrudnik - -## [v1.31.0](https://github.com/zenstruck/foundry/releases/tag/v1.31.0) - -March 27th, 2023 - [v1.30.3...v1.31.0](https://github.com/zenstruck/foundry/compare/v1.30.3...v1.31.0) - -* ac35acf fix: nested factories not persisting should not throw error (#444) by @nikophil -* d346c68 feat: Have delayFlush return the callback's return (#442) by @HypeMC - -## [v1.30.3](https://github.com/zenstruck/foundry/releases/tag/v1.30.3) - -March 22nd, 2023 - [v1.30.2...v1.30.3](https://github.com/zenstruck/foundry/compare/v1.30.2...v1.30.3) - -* c31d653 fix(RepositoryProxy::find()): allow not entity object in (#441) by @nikophil - -## [v1.30.2](https://github.com/zenstruck/foundry/releases/tag/v1.30.2) - -March 22nd, 2023 - [v1.30.1...v1.30.2](https://github.com/zenstruck/foundry/compare/v1.30.1...v1.30.2) - -* 2f71013 fix: allow any object passed as find() criteria (#440) by @nikophil - -## [v1.30.1](https://github.com/zenstruck/foundry/releases/tag/v1.30.1) - -March 21st, 2023 - [v1.30.0...v1.30.1](https://github.com/zenstruck/foundry/compare/v1.30.0...v1.30.1) - -* fba0933 fix: regression from embedded object in findOrCreate function (#438) by @nicolasne, Nicolas NΓ©non, @kbond - -## [v1.30.0](https://github.com/zenstruck/foundry/releases/tag/v1.30.0) - -March 20th, 2023 - [v1.29.0...v1.30.0](https://github.com/zenstruck/foundry/compare/v1.29.0...v1.30.0) - -* c5ca551 refactor: deprecate AnonymousFactory class and factory() helper (#436) by @nikophil -* 837dbf6 fix(global state): register story as service in StoryManager (#434) by @nikophil -* 230f75e feat: findOrCreate() can use embeddables (#432) by @nikophil -* cacf9ea minor: add conflict for `doctrine/mongodb-odm` for false deprecation (#435) by @kbond -* 6cc9541 fix: running `array_values()` on array attribute (#435) by @kbond -* 083a9b9 bug: add failing test for attribute that is array (#435) by @kbond -* 0094f09 refactor: simplify atrtibutes normalization (#423) by @nikophil -* d8a94ce doc: Add introduction to `index.rst` for symfony.com accessibility (#429) by @GromNaN - -## [v1.29.0](https://github.com/zenstruck/foundry/releases/tag/v1.29.0) - -February 28th, 2023 - [v1.28.0...v1.29.0](https://github.com/zenstruck/foundry/compare/v1.28.0...v1.29.0) - -* 041b597 feat: add `LazyValue` to calculate attribute values only when needed (#427) by @kbond, @mpdude -* 5f4f679 feat(bundle): allow registering custom Faker Providers (#425) by @kbond -* b0bd5cb fix: restore Factory::$cascadePersist (#424) by @nikophil -* f8db2af minor: remove `Factory::$cascadePersist` (#422) by @nikophil -* edcd083 fix(doc): return type syntax (#420) by @benblub, @OskarStark -* bfb1134 feat: validate generate factory with static analysis (#419) by @nikophil -* a7502e7 docs: show how to document magic methods in stories (#418) by @nikophil -* bbc3b62 fix(ci): don't run fixcs/sync-with-template on forks (#417) by @kbond -* c409dc7 minor(ci): drop testing unsupported Symfony versions (6.0/6.1) (#417) by @kbond -* 626acba fix: foundry should work witout maker bundle (#416) by @nikophil -* 0ff998a chore: migrate phpunit config-file (dama) for phpunit 9 format (#413) by @Chris53897, @Chris8934 -* 30e0bdb feat(make:factory): match directory/namespace structure (#411) by @nikophil -* a6d5ead docs: Replace annotations with attributes in docs (#412) by @ker0x - -## [v1.28.0](https://github.com/zenstruck/foundry/releases/tag/v1.28.0) - -January 24th, 2023 - [v1.27.0...v1.28.0](https://github.com/zenstruck/foundry/compare/v1.27.0...v1.28.0) - -* e54c757 fix: remove type in closure in RepositoryProxy::proxyResult() (#410) by @nikophil -* 6f54bbb chore: easy way to require lowest deps (#407) by @nikophil -* 0a4b4ec feat: drop support of symfony 4, remove deprecation for console/command (#398) by @Chris53897, @Chris8934 -* 467f62f feat(make:factory): handle name collision (#402) by @nikophil -* 1c96c86 chore: migrate phpunit config-file for phpunit 9 format (#404) by @Chris53897, @Chris8934 -* aac5aba docs: fix spelling of annotations in CHANGELOG.md (#403) by @Chris53897, @Chris8934 -* 2875dd7 feat(make:factory): list embeddables (#400) by @nikophil -* 3a40f1a minor: meaningful error for faker in data provider (#399) by @nikophil -* 48398f1 feat(make:factory): default value for enum types (#393) by @nikophil -* 6cf06c4 tests: reactivate self deprecations (#392) by @nikophil - -## [v1.27.0](https://github.com/zenstruck/foundry/releases/tag/v1.27.0) - -January 9th, 2023 - [v1.26.0...v1.27.0](https://github.com/zenstruck/foundry/compare/v1.26.0...v1.27.0) - -* 7b97ac2 feat: add $criteria param to RepositoryAssertions::empty() (#391) by @nikophil - -## [v1.26.0](https://github.com/zenstruck/foundry/releases/tag/v1.26.0) - -December 29th, 2022 - [v1.25.0...v1.26.0](https://github.com/zenstruck/foundry/compare/v1.25.0...v1.26.0) - -* 79913c3 feat: create parameter to RepositoryAssertions::count() methods (#390) by @nikophil -* 4df0f40 chore: improve makefile (#382) by @nikophil -* 49da6a0 feat(make:factory): use autocompletion for no persistence classes (#383) by @nikophil - -## [v1.25.0](https://github.com/zenstruck/foundry/releases/tag/v1.25.0) - -December 22nd, 2022 - [v1.24.1...v1.25.0](https://github.com/zenstruck/foundry/compare/v1.24.1...v1.25.0) - -* b101604 fix: ci by @kbond -* cc22eac chore: fix cs (#386) by @kbond -* e0944bb chore(ci): sync meta files and automate cs fixer (#386) by @kbond -* d970d7a minor: Reference non deprecated method (#387) by @jongotlin -* 7ab0740 minor(story): make `Story::getState()` protected (#385) by @kbond -* 9a6f28e chore: availability to chose php version (#376) (#381) by @nikophil -* aaeb6cf doc: display downloads badge (#380) by @kbond -* e4d5fcb chore(ci): test on PHP 8.2 (#361) by @kbond -* 555d547 chore: change test context with .env (#375) by @nikophil -* 2eb52ed feat(make:factory): auto create missing factories (#372) by @nikophil -* 6bc81b1 refactor: set all fixtures class name unique (#374) by @nikophil -* 892ed14 feat(make:factory): improve Doctrine default fields guesser (#364) by @nikophil -* 7b08360 doc: fix header (#370) by @seb-jean - -## [v1.24.1](https://github.com/zenstruck/foundry/releases/tag/v1.24.1) - -November 29th, 2022 - [v1.24.0...v1.24.1](https://github.com/zenstruck/foundry/compare/v1.24.0...v1.24.1) - -* 6588804 dependencies: allow symfony/string 5.4 (#369) by @HypeMC -* 9e5450e docs: fix namespaces in global_state example (#366) by @OskarStark -* 917aba5 docs: fix onfig value (#368) by @OskarStark -* 7773f7e docs: fix config key (#367) by @OskarStark - -## [v1.24.0](https://github.com/zenstruck/foundry/releases/tag/v1.24.0) - -November 25th, 2022 - [v1.23.0...v1.24.0](https://github.com/zenstruck/foundry/compare/v1.23.0...v1.24.0) - -* f5e9eae minor: use --no-persistence instead of --not-persisted (#365) by @nikophil -* 730c0d9 chore: rename service ids (#363) by @kbond -* 19acc72 feat: add `RepositoryProxy::inner()` (#362) by @kbond -* a003bac refactor(make:factory): split command with DefaultPropertiesGuesser (#357) by @nikophil -* d8eca88 chore(ci): test on Symfony 6.2 (#359) by @kbond -* 20ac349 refactor(make:factory): use value object to render template (#354) by @nikophil -* 4e5f9d9 feat(make:factory): use factories to default non-nullable relationships (#351) by @nikophil, @benblub -* 8332956 feat: make `Story::get()` static (implies `Story::load()->get()`) (#253) by @kbond -* b89bcff minor(make:factory): misc enhancements of maker (#345) by @nikophil -* 3cc95a5 minor: remove php 7.4 related tests (#349) by @nikophil -* 8a055b0 chore: fix docker cache (#350) by @nikophil -* 18ea4fb feat(make:factory): create factory for not-persisted objects (#343) by @nikophil -* 64786fc fix: typo in docs (#348) by @nikophil -* 1a98fc4 chore: Use composer 2.4 (#346) by @OskarStark -* 96c4cbe minor(make:factory): Use `@see`/`@todo` annotations (#344) by @OskarStark -* cbeb2ce fix: adjust docblocks to remove PhpStorm errors (#341) by @kbond -* cd1e394 fix: use orm limit length in factory (#294) by @MrYamous -* b1d7ce3 [feature] add default for Mongo properties in (#340) by @nikophil -* 778607a [chore] use cache for docker CI (#339) by @nikophil -* c662eb3 [feature] auto add phpstan annotations in make:factory (#338) by @nikophil -* 2bd046f [docs] sort phpstan-method annotations (#333) by @OskarStark -* 0460741 [docs] remove obsolete section (#335) by @nikophil -* 2c34baf [bug] Typos in Makefile (#330) by @OskarStark -* 65924b2 [bug] typos in docs (#331) by @OskarStark -* 6b48878 [chore] upgrade ci actions (#329) by @kbond -* 210faff [chore] use phpstan instead of psalm (#328) by @nikophil -* 51f1bc0 [refactor] modernize code with rector (#327) by @nikophil -* 8423b75 [chore] adjust `.symfony.bundle.yaml` for new branch (#325) by @kbond -* 5a05513 [feature] require php8+ (#327) by @kbond - -## [v1.23.0](https://github.com/zenstruck/foundry/releases/tag/v1.23.0) - -November 10th, 2022 - [v1.22.1...v1.23.0](https://github.com/zenstruck/foundry/compare/v1.22.1...v1.23.0) - -* f43b067 [chore] clean up CI (#324) by @nikophil -* 3588274 [feature] Allow to use foundry without Doctrine (#323) by @nikophil -* 7598467 [feature] [remove bundleless usge] configure global state with config (#322) by @nikophil -* e417945 [feature] [remove bundleless usge] use config instead of environment variables (#320) by @nikophil -* cada0cf [feature] pass an index to `FactoryCollection` attributes (#318) by @nikophil -* d120b1c [minor] fix `bamarni/composer-bin-plugin` deprecations (#313) by @kbond -* a3eefc1 [minor] remove branch alias (#313) by @kbond -* cf7d75e [minor] remove unneeded bin script (#310) by @kbond -* cd42774 [feature] add make migrations (#309) by @nikophil -* cb9a4ec [feature] add a docker stack (#306) by @nikophil - -## [v1.22.1](https://github.com/zenstruck/foundry/releases/tag/v1.22.1) - -September 28th, 2022 - [v1.22.0...v1.22.1](https://github.com/zenstruck/foundry/compare/v1.22.0...v1.22.1) - -* 8d41ca8 [bug] discover relations with inheritance (#300) by @NorthBlue333 -* ae6bda2 [bug] multiple relationships with same entity (#302) by @NorthBlue333 - -## [v1.22.0](https://github.com/zenstruck/foundry/releases/tag/v1.22.0) - -September 21st, 2022 - [v1.21.1...v1.22.0](https://github.com/zenstruck/foundry/compare/v1.21.1...v1.22.0) - -* 4fb5fb8 [feature] Introduce Sequences (#298) by @nikophil - -## [v1.21.1](https://github.com/zenstruck/foundry/releases/tag/v1.21.1) - -September 12th, 2022 - [v1.21.0...v1.21.1](https://github.com/zenstruck/foundry/compare/v1.21.0...v1.21.1) - -* 3b105a7 [bug] Fix usage of faker dateTime in factory maker (#297) by @jmsche -* 0663f29 [doc] Fix code block docs about faker seed (#296) by @jmsche -* b57d067 [doc] fix typo (#295) by @Chris53897 -* 4577ef4 [minor] Improve deprecation message for `createMany()` (#291) by @gazzatav, @kbond - -## [v1.21.0](https://github.com/zenstruck/foundry/releases/tag/v1.21.0) - -June 27th, 2022 - [v1.20.0...v1.21.0](https://github.com/zenstruck/foundry/compare/v1.20.0...v1.21.0) - -* e02fbe1 [doc] update config for 5.4+ (#285) by @kbond -* 39258de [feature] add configuration option for faker generator seed (#285) by @kbond -* 1bd05ce [feature] re-save created object after "afterPersist" events called (#279) by @kbond -* 195c815 [bug] Use DateTimeImmutable with immutable ORM types (#283) by @HypeMC - -## [v1.20.0](https://github.com/zenstruck/foundry/releases/tag/v1.20.0) - -June 20th, 2022 - [v1.19.0...v1.20.0](https://github.com/zenstruck/foundry/compare/v1.19.0...v1.20.0) - -* 6009499 [feature] add `Story::getPool()` (#282) by @kbond - -## [v1.19.0](https://github.com/zenstruck/foundry/releases/tag/v1.19.0) - -May 24th, 2022 - [v1.18.2...v1.19.0](https://github.com/zenstruck/foundry/compare/v1.18.2...v1.19.0) - -* 46de01a [feature] Handle variadic constructor arguments (#277) by @ndench -* f5d9177 [minor] use symfony/phpunit-bridge 6+ by @kbond -* 09b0ae2 [minor] fix sca by @kbond - -## [v1.18.2](https://github.com/zenstruck/foundry/releases/tag/v1.18.2) - -April 29th, 2022 - [v1.18.1...v1.18.2](https://github.com/zenstruck/foundry/compare/v1.18.1...v1.18.2) - -* 2b2d2e7 [minor] allow `doctrine/persistence` 3 (#275) by @kbond -* 429466e [doc] add note about phpstan docblocks (#274) by @kbond, Jacob Dreesen - -## [v1.18.1](https://github.com/zenstruck/foundry/releases/tag/v1.18.1) - -April 22nd, 2022 - [v1.18.0...v1.18.1](https://github.com/zenstruck/foundry/compare/v1.18.0...v1.18.1) - -* ff9e4ef [bug] fix embeddable support when used with file (ie xml) mapping (#271) by @kbond -* 40a5a1e [minor] support Symfony 6.1 (#267) by @kbond - -## [v1.18.0](https://github.com/zenstruck/foundry/releases/tag/v1.18.0) +## [v2.0.5](https://github.com/zenstruck/foundry/releases/tag/v2.0.5) -April 11th, 2022 - [v1.17.0...v1.18.0](https://github.com/zenstruck/foundry/compare/v1.17.0...v1.18.0) +July 3rd, 2024 - [v2.0.4...v2.0.5](https://github.com/zenstruck/foundry/compare/v2.0.4...v2.0.5) -* b9d2ed3 [feature] add `Factory::delayFlush()` (#84) by @kbond -* 91609b4 [minor] remove scrutinizer (#266) by @kbond -* 8117f40 [minor] allow `dama/doctrine-test-bundle` 7.0 (#266) by @kbond -* 6052e81 [minor] Revert "[bug] fix global state with symfony/framework-bundle >= 5.4.6/6.0.6" (#260) by @kbond +* 6105a36 fix: make proxy work with last symfony/var-exporter version (#664) by @nikophil +* e8623a3 [DOC] Fix Upgrade Guide URL Rendering (#654) by @cezarpopa +* f7f133a fix: create ArrayCollection if needed (#645) by @nikophil +* 779bee4 fix: after_flush() can use objects created in global state (#653) by @nikophil +* 72e48bf tests(ci): add test permutation for PHPUnit >= 10 (#647) by @nikophil +* 1edf948 docs: fix incoherence (#652) by @nikophil +* 1c66e39 minor: improve repository assertion messages (#651) by @nikophil -## [v1.17.0](https://github.com/zenstruck/foundry/releases/tag/v1.17.0) +## [v2.0.4](https://github.com/zenstruck/foundry/releases/tag/v2.0.4) -March 24th, 2022 - [v1.16.0...v1.17.0](https://github.com/zenstruck/foundry/compare/v1.16.0...v1.17.0) +June 20th, 2024 - [v2.0.3...v2.0.4](https://github.com/zenstruck/foundry/compare/v2.0.3...v2.0.4) -* c131715 [bug] fix global state with symfony/framework-bundle >= 5.4.6/6.0.6 (#259) by @kbond -* 0edbea8 [minor] remove Symfony 5.3 from test matrix (#259) by @kbond -* 5768345 [feature] add Story "pools" (#252) by @kbond -* be6b6c8 Revert "[feature] Allow any type for Story States (#231)" (#252) by @kbond -* 02cd0c8 [minor] deprecate `Story:add()` and add `Story::addState()` (#254) by @kbond -* 02609a9 [minor] add return type for stub command (deprecated in symfony 6) (#257) by @Chris53897, Christopher Georg -* 6977f3a [doc] Use `UserPasswordHasherInterface` instead of `UserPasswordEncoderInterface` (#255) by @zairigimad -* 01ebfab [feature] add an 'All' option to make:factory to create all missing factories (#247) by @abeal-hottomali -* 39fa8e2 [bug] ignore abstract classes in the maker (#249) by @abeal-hottomali -* 62eeb75 [minor] run php-cs-fixer on php 7.2 (#243) by @kbond +* 0989c5d fix: don't try to proxify objects that are not persistable (#646) by @nikophil +* 50ae3dc fix: handle contravariance problem when proxifying class with unserialize method (#644) by @nikophil -## [v1.16.0](https://github.com/zenstruck/foundry/releases/tag/v1.16.0) +## [v2.0.3](https://github.com/zenstruck/foundry/releases/tag/v2.0.3) -January 6th, 2022 - [v1.15.0...v1.16.0](https://github.com/zenstruck/foundry/compare/v1.15.0...v1.16.0) +June 19th, 2024 - [v1.38.2...v2.0.3](https://github.com/zenstruck/foundry/compare/v1.38.2...v2.0.3) -* 79261d6 [feature] MongoDB ODM Support (#153) by @kbond, @nikophil -* d97d895 [minor] fix psalm (#232) by @kbond -* fc74f26 [minor] add allow-plugins for composer 2.2+ (#232) by @kbond - -## [v1.15.0](https://github.com/zenstruck/foundry/releases/tag/v1.15.0) - -December 30th, 2021 - [v1.14.1...v1.15.0](https://github.com/zenstruck/foundry/compare/v1.14.1...v1.15.0) - -* fb79022 [feature] Allow any type for Story States (#231) by @wouterj -* d6d7d52 [doc] update url (#230) by @bfoks -* 4915b61 [doc] Fix event hook argument name (#229) by @Aeet, @kbond -* 7e13ed0 [doc] add note about how attributes are built (#228) by @gnito-org -* 552dc6f [doc] Correct spelling in index.rst (#226) by @gnito-org -* 50a91b9 [bug] Fix smallint generated Faker (#223) by @jmsche -* 68552a7 [doc] Document the MakeFactory all-fields option (#220) by @gnito-org -* 93a2f9c [feature] Add all-fields option to MakeFactory (#218) by @gnito-org - -## [v1.14.1](https://github.com/zenstruck/foundry/releases/tag/v1.14.1) - -December 2nd, 2021 - [v1.14.0...v1.14.1](https://github.com/zenstruck/foundry/compare/v1.14.0...v1.14.1) - -* bf1cbc9 [minor] Bump symfony/http-kernel from 5.3.7 to 5.4.0 in /bin/tools/psalm (#217) by @dependabot[bot], dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> -* 433f9b2 [minor] allow symfony/deprecation-contracts 3+ by @kbond -* 2b10729 [minor] fix failing test by @kbond -* b3ce03f [minor] add void return type (#176) by @seb-jean - -## [v1.14.0](https://github.com/zenstruck/foundry/releases/tag/v1.14.0) - -November 13th, 2021 - [v1.13.4...v1.14.0](https://github.com/zenstruck/foundry/compare/v1.13.4...v1.14.0) - -* 46e968e [minor] add Symfony 6.0/PHP 8.1 support (#198) by @kbond - -## [v1.13.4](https://github.com/zenstruck/foundry/releases/tag/v1.13.4) - -October 21st, 2021 - [v1.13.3...v1.13.4](https://github.com/zenstruck/foundry/compare/v1.13.3...v1.13.4) - -* 676f00a [bug] disable migration transactions (#207) by @kbond -* c6df43d [ci] re-enable migration tests on php >= 8 (#207) by @kbond -* 2ef6c6a [ci] disable migration tests on php >= 8 (#206) by @kbond -* e29a008 [bug] fix maker auto-defaults with yaml driver (#205) by @domagoj-orioly -* c483b2e [ci] use reusable workflows where possible (#203) by @kbond -* 886204f [minor] adjust CI output by @kbond -* 63a956e [minor] use zenstruck/assert for assertions instead of phpunit (#182) by @kbond - -## [v1.13.3](https://github.com/zenstruck/foundry/releases/tag/v1.13.3) - -September 24th, 2021 - [v1.13.2...v1.13.3](https://github.com/zenstruck/foundry/compare/v1.13.2...v1.13.3) - -* 477db0a [minor] install psalm as composer-bin tool (#199) by @kbond -* 6ced887 [minor] add Symfony 5.4 to test matrix (#197) by @kbond -* 6610c5d [bug] rename "rank" as it's a reserved keyword in mysql 8 (#197) by @kbond - -## [v1.13.2](https://github.com/zenstruck/foundry/releases/tag/v1.13.2) - -September 3rd, 2021 - [v1.13.1...v1.13.2](https://github.com/zenstruck/foundry/compare/v1.13.1...v1.13.2) - -* 06b24d4 [bug] when creating collections, check for is persisting first (#195) by @jordisala1991, @kbond - -## [v1.13.1](https://github.com/zenstruck/foundry/releases/tag/v1.13.1) - -August 31st, 2021 - [v1.13.0...v1.13.1](https://github.com/zenstruck/foundry/compare/v1.13.0...v1.13.1) - -* 1dccda1 [bug] fix/improve embeddable support (#193) by @kbond -* 5f39d8a [minor] update symfony-tools/docs-builder by @kbond - -## [v1.13.0](https://github.com/zenstruck/foundry/releases/tag/v1.13.0) - -August 30th, 2021 - [v1.12.0...v1.13.0](https://github.com/zenstruck/foundry/compare/v1.12.0...v1.13.0) - -* 39f69f9 [doc] update symfony.com links (#191) by @kbond -* 31b7569 [doc] switch documentation to symfony.com bundle doc format (#190) by @kbond, @wouterj -* d4943d7 [minor] exclude maker templates from phpunit code coverage (#189) by @kbond -* 60f78b3 [doc] add note about simplified factory annotations in PhpStorm 2021.2+ (#189) by @kbond -* 8769d7a [minor] update factory maker template annotations (#189) by @kbond -* 1faf97d [feature] persisting factories respect cascade persist (#181) by @mpiot -* 0416dc4 [minor] Be able to remove most of method annotations on user factories (#185) by @Nyholm -* 468e80b [minor] add missing ->expectDeprecation() to legacy tests (#188) by @kbond -* 1647e1b [bug] ensure legacy test works as expected (#187) by @kbond -* a6f6413 [minor] Added .editorconfig to sync up styles (#186) by @Nyholm -* 2517f54 [minor] change Instantiator::$forceProperties type-hint (#183) by @kbond -* b145059 [minor] psalm fix (#180) by @kbond - -## [v1.12.0](https://github.com/zenstruck/foundry/releases/tag/v1.12.0) - -July 6th, 2021 - [v1.11.1...v1.12.0](https://github.com/zenstruck/foundry/compare/v1.11.1...v1.12.0) - -* 6b97f0f [minor] refactor make:factory auto-default feature (#174) by @kbond -* 2a0bbce [feature] Auto populate ModelFactory::getDefaults() from doctrine mapping (#173) by @benblub - -## [v1.11.1](https://github.com/zenstruck/foundry/releases/tag/v1.11.1) - -June 25th, 2021 - [v1.11.0...v1.11.1](https://github.com/zenstruck/foundry/compare/v1.11.0...v1.11.1) - -* ccac05c [minor] disable codecov pr annotations (#172) by @kbond -* 5c3abe2 [bug] allow passing full namespace with make:factory (#171) by @kbond - -## [v1.11.0](https://github.com/zenstruck/foundry/releases/tag/v1.11.0) - -June 4th, 2021 - [v1.10.0...v1.11.0](https://github.com/zenstruck/foundry/compare/v1.10.0...v1.11.0) - -* d2cd4c7 [feature] customize namespace for factories generated with make:factory (#164) by @kbond -* b9c161b [minor] suppress psalm error (#165) by @kbond -* 112d57d [feature] make:factory only lists entities w/o factories (#162) by @jschaedl -* 6001e7e [minor] Detect missing maker bundle and suggest installation via StubCommands (#161) by @jschaedl -* cdab96e [minor] upgrade to php-cs-fixer 3 (#159) by @kbond -* d357215 [minor] update php-cs-fixer config (#159) by @kbond -* 3ce7da0 [minor] Update .gitattributes file (#158) by @ker0x -* be1d899 [doc] Fix example $posts for Attributes section (#155) by @babeuloula - -## [v1.10.0](https://github.com/zenstruck/foundry/releases/tag/v1.10.0) - -April 19th, 2021 - [v1.9.1...v1.10.0](https://github.com/zenstruck/foundry/compare/v1.9.1...v1.10.0) - -* 7dc49f0 [minor] unlock php-cs-fixer in gh action by @kbond -* e800c83 [feature] add option to use doctrine migrations to reset database (#145) by @kbond -* 8466067 [doc] fix small typo in docs (#147) by @nikophil -* 69fe2a6 [doc] fix typo (#146) by @AntoineRoue -* 463d32a [minor] fix faker deprecations by @kbond -* aa6b32a [minor] lock php-cs-fixer version in ci (bug in latest release) by @kbond -* 4dc13e6 [minor] adjust codecov threshold by @kbond -* cd5cefe [minor] use SHELL_VERBOSITY to hide logs during tests by @kbond - -## [v1.9.1](https://github.com/zenstruck/foundry/releases/tag/v1.9.1) - -March 19th, 2021 - [v1.9.0...v1.9.1](https://github.com/zenstruck/foundry/compare/v1.9.0...v1.9.1) - -* 0ebf0dd [bug] fix false positive for auto-refresh deprecation (fixes #141) (#143) by @kbond -* 7e693ed [doc] remove unnecessary notes about rebooting kernel (ref: #140) (#142) by @kbond -* ba7947c [minor] improve make:factory error message when no entities exist (#139) by @kbond -* 3812def [minor] use project var dir for test cache/logs (#139) by @kbond - -## [v1.9.0](https://github.com/zenstruck/foundry/releases/tag/v1.9.0) - -March 12th, 2021 - [v1.8.0...v1.9.0](https://github.com/zenstruck/foundry/compare/v1.8.0...v1.9.0) - -* 0872be0 [doc] Add --test option as tip (#138) by @OskarStark -* f55afe2 [doc] Fix typos in the docs (#136) by @jdreesen -* 88c081e [minor] "require" explicitly configuring global auto_refresh_proxies (#131) by @kbond -* 632de3d [feature] throw exception during autorefresh if unsaved changes detected (#131) by @kbond -* 5318c7f [minor] deprecate instantiating Factory directly: (#134) by @kbond -* b7a7880 [feature] add AnonymousFactory (#134) by @kbond -* 228895f [minor] add dev stability to ci matrix (#133) by @kbond -* b759712 [minor] explicitly add sqlite extension for gh actions (#130) by @kbond -* 1f332ed [bug] Fix exception message (#129) by @jdreesen -* 4bc80fb [minor] increase codecov threshold by @kbond - -## [v1.8.0](https://github.com/zenstruck/foundry/releases/tag/v1.8.0) - -February 27th, 2021 - [v1.7.1...v1.8.0](https://github.com/zenstruck/foundry/compare/v1.7.1...v1.8.0) - -* 83d6b26 [feature] add ModelFactory::assert()/RepositoryProxy::assert() (#123) by @kbond -* a657d14 [feature] add ModelFactory::all()/find()/findBy() (#123) by @kbond -* ac775b9 [feature] add ModelFactory::count()/truncate() (#123) by @kbond -* 34373da [feature] add ModelFactory::first()/last() (#123) by @kbond -* 5978574 [minor] psalm fixes (#122) by @kbond -* 88cb7c9 [minor] add getCommandDescription() to Maker's (#121) by @kbond -* 31971e0 [minor] fail ci if direct deprecations (#121) by @kbond -* ecc0e10 [bug] bump min php version (fixes #118) (#119) by @kbond - -## [v1.7.1](https://github.com/zenstruck/foundry/releases/tag/v1.7.1) - -February 6th, 2021 - [v1.7.0...v1.7.1](https://github.com/zenstruck/foundry/compare/v1.7.0...v1.7.1) - -* 6bab709 [bug] fix unmanaged many-to-one entity problem (fixes #114) (#117) by @kbond -* b92a69a [minor] adjust cs-check gh action and fix cs (#116) by @kbond - -## [v1.7.0](https://github.com/zenstruck/foundry/releases/tag/v1.7.0) - -January 17th, 2021 - [v1.6.0...v1.7.0](https://github.com/zenstruck/foundry/compare/v1.6.0...v1.7.0) - -* 9d42401 [feature] add attributes to ModelFactory/RepositoryProxy random methods (#112) by @kbond -* 149ea48 [feature] Remove "visual duplication" of ModelFactory::new()->create() (#111) by @wouterj -* 0c69967 [feature] Added ModelFactory::randomOrCreate() (#108) by @wouterj -* 574c246 [minor] use zenstruck/callback for Proxy::executeCallback() (#107) by @kbond -* 07f1ffe [minor] apply suggested psalm fix (#102) by @kbond -* e53b834 [minor] enable code coverage action to work with xdebug 3 (#99) by @kbond -* 6ea273b [minor] psalm-suppress InternalMethod (#99) by @kbond -* 1dbbab8 [minor] add codecov badge (#98) by @kbond -* 77f7ce0 [minor] switch to codecov by @kbond -* f269dc4 [minor] use ramsey/composer-install in static-analysis job (#97) by @kbond -* 0ca5479 [minor] Streamline GitHub CI by using ramsey/composer-install (#96) by @wouterj -* 8511d7a [minor] Re-enable Psalm and fixed annotations (#95) by @wouterj, @kbond - -## [v1.6.0](https://github.com/zenstruck/foundry/releases/tag/v1.6.0) - -December 7th, 2020 - [v1.5.0...v1.6.0](https://github.com/zenstruck/foundry/compare/v1.5.0...v1.6.0) - -* c3f38d2 [minor] use local kernel instance in Factories and ResetDatabase traits (#92) by @kbond -* 88db502 [doc] document the need to create test client before factories (#92) by @kbond -* bf4d47a [bug] ensure foundry isn't rebooted in DatabaseReset (#92) by @kbond -* 1b6231a [bug] ensure kernel shutdown after ResetDatabase::_resetSchema() (#92) by @kbond -* 63c8eb7 [minor] disable psalm static analysis pending fix (#71) by @kbond -* e4d0a06 [doc] Added another relation example to Many-To-One (#93) by @weaverryan -* 596af47 [minor] support php8 (#71) by @kbond -* 2d574a5 [doc] Added docs for ModelFactory::new() (#91) by @Nyholm -* 6bd3195 [doc] Update link to faker (#90) by @Nyholm -* 338f6c8 [minor] Do not turn Psalm PHPdocs into comments (#85) by @wouterj -* dfc4388 [minor] Fixed issues found by Psalm level 4 (#85) by @wouterj -* 99aa22a [minor] Suppress nullable Psalm level 5 error (#85) by @wouterj -* e4ea180 [minor] Fixed issues found by Psalm level 6 (#85) by @wouterj -* 72c2e77 [minor] Added Psalm templated annotations (#85) by @wouterj -* 48572ce [minor] Installed Psalm and configured GitHub Workflow (#85) by @wouterj -* 0476572 [minor] Update docs with PHP file config (#87) by @TavoNiievez -* 66d0025 [minor] Use PHP CS Fixer udiff to only show snippets (#86) by @wouterj -* 8ecb162 [minor] Use consistent spacing in GitHub Actions config (#86) by @wouterj -* 7e90a05 [minor] Only run one build with prefer-lowest (#86) by @wouterj - -## [v1.5.0](https://github.com/zenstruck/foundry/releases/tag/v1.5.0) - -November 10th, 2020 - [v1.4.0...v1.5.0](https://github.com/zenstruck/foundry/compare/v1.4.0...v1.5.0) - -* 3b36c9f [minor] deprecate using snake/kebab-cased attributes (#81) by @kbond -* 3ecd4f6 [minor] set min version of symfony/maker-bundle to 1.13.0 (#81) by @kbond -* 3c0e149 [minor] swap phpunit for phpunit-bridge mark deprecated tests (#81) by @kbond -* 4292d02 [bug] fix typo (#81) by @kbond -* de1fb41 [minor] trigger deprecations for other deprecated code (#81) by @kbond -* 5134347 [minor] deprecate "optional:" & "force:" attribute prefixes (#81) by @kbond -* def8ebc [feature] define extra attributes/forced properties on Instantiator (#81) by @kbond -* da504e0 [bug] boolean nodes should default to false instead of null (#83) by @kbond -* c376fef [minor] sort available entities (#80) by @wouterj -* d3a32cf [minor] add static return annotations (#79) by @micheh -* 77a3583 [minor] run test suite on PostgreSQL (#51) by @kbond -* 8db03c8 [minor] change faker lib used (#70) by @kbond -* 188eb63 [minor] disable dependabot by @kbond -* ea8fefe [minor] bump actions/cache from v1 to v2.1.2 (#69) by @dependabot[bot], dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> -* bd5249d [minor] update shivammathur/setup-php requirement to 2.7.0 (#68) by @dependabot[bot], dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> -* 800fae7 [minor] update actions/checkout requirement to v2.3.3 (#67) by @dependabot[bot], dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> -* 0252e90 [minor] add dependabot for github actions by @kbond -* 537b2fd [bug] RepositoryProxy::findOneBy() with $orderBy checks inner repo (#66) by @kbond -* 9302855 [minor] RepositoryProxy::truncate() compatible with any ObjectManager (#66) by @kbond -* 9fe12e5 [minor] have RepositoryProxy implement \Countable & \IteratorAggregate (#66) by @kbond -* 1c43645 [feature] improve RespositoryProxy::first() and add last() (#64) by @mpiot -* f9418ac [bug] add $orderBy param to RepositoryProxy::findOneBy() (#63) by @mpiot - -## [v1.4.0](https://github.com/zenstruck/foundry/releases/tag/v1.4.0) - -October 20th, 2020 - [v1.3.0...v1.4.0](https://github.com/zenstruck/foundry/compare/v1.3.0...v1.4.0) - -* d5cab62 [doc] fixes (#59) by @kbond -* 4eb546c [doc] document non-Kernel testing (#59) by @kbond -* c8040f7 [doc] document using in PHPUnit data providers (#59) by @kbond -* fce3610 [minor] throw helpful exception if creating service factories w/o boot (#59) by @kbond -* a904e41 [minor] throw helpful exception if using service factories w/o bundle (#59) by @kbond -* 1efc502 [minor] throw helpful exception if using service stories without bundle (#59) by @kbond -* a5d4154 [feature] remove ->withoutPersisting() requirement in pure unit tests (#59) by @kbond -* aef9123 [feature] allow Factories trait to be used in pure units tests (#59) by @kbond -* 732e616 [minor] deprecate TestState::withoutBundle() (#59) by @kbond -* d2c8b47 [bug] ensure Foundry is "shutdown" after each test (#59) by @kbond -* f3cc0c3 [bug] allow model factories to be created in dataProviders (#59) by @kbond -* b80d778 [doc] adding a link to SymfonyCasts (#60) by @weaverryan -* e37f492 [minor] add script to run all test configurations locally by @kbond - -## [v1.3.0](https://github.com/zenstruck/foundry/releases/tag/v1.3.0) - -October 14th, 2020 - [v1.2.1...v1.3.0](https://github.com/zenstruck/foundry/compare/v1.2.1...v1.3.0) - -* fd433b1 [feature] allow factories to be defined as services (#53) by @kbond -* e686a08 [minor] remove dead debug code (#57) by @kbond -* aaf6ab4 [bug] fix typo in Factory stub (fixes #52) (#57) by @kbond - -## [v1.2.1](https://github.com/zenstruck/foundry/releases/tag/v1.2.1) - -October 12th, 2020 - [v1.2.0...v1.2.1](https://github.com/zenstruck/foundry/compare/v1.2.0...v1.2.1) - -* ecf674a [doc] note that the ResetDatabase trait is required for global state by @kbond -* b748615 [minor] ensure coverage jobs use dama bundle (#48) by @kbond -* d4e3a2e [bug] sqlite does not support --if-exists (fixes #46) (#48) by @kbond -* 1138cf0 [minor] add sqlite tests (#48) by @kbond -* 91a6032 [minor] adjust github actions to use DATABASE_URL env var (#48) by @kbond - -## [v1.2.0](https://github.com/zenstruck/foundry/releases/tag/v1.2.0) - -October 8th, 2020 - [v1.1.4...v1.2.0](https://github.com/zenstruck/foundry/compare/v1.1.4...v1.2.0) - -* e7b8481 [feature] add FactoryCollection object to help with relationships (#38) by @kbond - -## [v1.1.4](https://github.com/zenstruck/foundry/releases/tag/v1.1.4) - -October 7th, 2020 - [v1.1.3...v1.1.4](https://github.com/zenstruck/foundry/compare/v1.1.3...v1.1.4) - -* 60e6881 [bug] allow RepositoryProxy::proxyResult() to handle doctrine proxies (#43) by @kbond - -## [v1.1.3](https://github.com/zenstruck/foundry/releases/tag/v1.1.3) - -September 28th, 2020 - [v1.1.2...v1.1.3](https://github.com/zenstruck/foundry/compare/v1.1.2...v1.1.3) - -* 118186d [bug] ensure all attributes passed to afterPersist events (fixes #31) (#40) by @kbond -* f054e3c [bug] allow array callables in Proxy::executeCallback() (#39) by @kbond - -## [v1.1.2](https://github.com/zenstruck/foundry/releases/tag/v1.1.2) - -September 8th, 2020 - [v1.1.1...v1.1.2](https://github.com/zenstruck/foundry/compare/v1.1.1...v1.1.2) +* 6f0835f fix(2.x): only reset error handler in before class hook (#643) by @nikophil +* 3c31193 test: add test with multiple ORM schemas (#629) by @vincentchalamon +* 303211a fix: unproxy args in proxy objects (#635) by @nikophil +* b76c294 fix(2.x): support Symfony 7.1 (#622) by @nikophil +* 9cd97b7 docs: Improve DX for tests (#636) by @matthieumota +* 17b0228 fix(2.x): add back second parameter for after persist callbacks (#631) by @nikophil +* 0c7b3af docs: Fix typo in the upgrade guide (#624) by @stof +* 933ebbd docs: upgrade readme with a link to upgrade guide (#620) by @nikophil +* 5f0ce76 Fix `Instantiator::allowExtra` example (#616) by @norkunas +* c2cbcbc fix(orm): reset database instead of dropping the schema when using migrations (#615) by @vincentchalamon +* 1393c13 docs: update docs for foundry v2 (#613) by @nikophil +* 0034c78 feat(2.x, make:facotry): add phpdoc conditionnally with --with-phpdoc option (#611) by @nikophil +* a3ec473 fix: prevent refresh error with autorefresh (#610) by @nikophil +* ca8258a minor(make:factory): default false for ODM mapping (#605) by @nikophil +* df5bb6b minor(2.x): remove APP_ENV=test from phpunit.xml (#603) by @nikophil +* 0479ddf fix(2.x): allow sequence with associative arrays (#595) by @nikophil +* d1509fb fix(2.x): support readonly entities (#599) by @nikophil +* 2c8e048 feat(2.x): allow to configure default namespace fo make:factory (#600) by @nikophil +* 9174dc6 fix: restore PHPUnit error handler (#587) by @nikophil +* 4156302 tests: asserts story works without persistence (#589) by @nikophil +* ec2c895 minor: add phpunit attributes (#576) by @nikophil +* 90cc839 feat(sequence): sequence attributes should be compatible with 1.x (#575) by @nikophil +* 60ec275 fix: sqlite with orm v2 (#574) by @kbond +* ae82186 feat: compatibility with ORM v3 (#572) by @nikophil, @kbond +* 624e8d2 feat: foundry 2.0 πŸŽ‰ by @nikophil +* e74f6b9 fix(rector) second argument for many() is optional (#515) by @nikophil +* a555474 fix(rector): repository method is static (#515) by @nikophil +* 53f25a2 rector: rewrite phpdoc (#571) by @nikophil +* ecbc615 refactor: Foundry 2 BC layer (#515) by @nikophil -* fb5b4ff [minor] run php-cs-fixer self-update (#33) by @kbond -* d734536 [minor] allow doctrine/persistence v2 (#33) by @kbond +## [v2.0.2](https://github.com/zenstruck/foundry/releases/tag/v2.0.2) -## [v1.1.1](https://github.com/zenstruck/foundry/releases/tag/v1.1.1) +June 14th, 2024 - [v2.0.1...v2.0.2](https://github.com/zenstruck/foundry/compare/v2.0.1...v2.0.2) -July 24th, 2020 - [v1.1.0...v1.1.1](https://github.com/zenstruck/foundry/compare/v1.1.0...v1.1.1) +* b76c294 fix(2.x): support Symfony 7.1 (#622) by @nikophil +* 9cd97b7 docs: Improve DX for tests (#636) by @matthieumota +* 17b0228 fix(2.x): add back second parameter for after persist callbacks (#631) by @nikophil +* 0c7b3af docs: Fix typo in the upgrade guide (#624) by @stof +* 933ebbd docs: upgrade readme with a link to upgrade guide (#620) by @nikophil -* 91af450 [doc] better document without persisting usage (closes #22) (#27) by @kbond -* 03bce0d [minor] improve "foundry not booted" exception message (closes #24) (#28) by @kbond -* 2d7bc47 [doc] fix test example (closes #25) (#26) by @kbond -* 0a4fa3a Update README.md (#23) by @kbond -* 576858a [doc] fix typo (#20) by @jdreesen -* 331716a [doc] add packagist version badge by @kbond +## [v2.0.1](https://github.com/zenstruck/foundry/releases/tag/v2.0.1) -## [v1.1.0](https://github.com/zenstruck/foundry/releases/tag/v1.1.0) +June 10th, 2024 - [v2.0.0...v2.0.1](https://github.com/zenstruck/foundry/compare/v2.0.0...v2.0.1) -July 11th, 2020 - [v1.0.0...v1.1.0](https://github.com/zenstruck/foundry/compare/v1.0.0...v1.1.0) +* 5f0ce76 Fix `Instantiator::allowExtra` example (#616) by @norkunas +* c2cbcbc fix(orm): reset database instead of dropping the schema when using migrations (#615) by @vincentchalamon -* 7d91e42 [minor] change composer "type" by @kbond -* c01374a [BC BREAK] moved bundle to src root so it can be auto-configured by flex by @kbond +## [v2.0.0](https://github.com/zenstruck/foundry/releases/tag/v2.0.0) -## [v1.0.0](https://github.com/zenstruck/foundry/releases/tag/v1.0.0) +June 7th, 2024 - [v1.38.0...v2.0.0](https://github.com/zenstruck/foundry/compare/v1.38.0...v2.0.0) -July 10th, 2020 - _[Initial Release](https://github.com/zenstruck/foundry/commits/v1.0.0)_ +* 1393c13 docs: update docs for foundry v2 (#613) by @nikophil +* 0034c78 feat(2.x, make:facotry): add phpdoc conditionnally with --with-phpdoc option (#611) by @nikophil +* a3ec473 fix: prevent refresh error with autorefresh (#610) by @nikophil +* ca8258a minor(make:factory): default false for ODM mapping (#605) by @nikophil +* df5bb6b minor(2.x): remove APP_ENV=test from phpunit.xml (#603) by @nikophil +* 0479ddf fix(2.x): allow sequence with associative arrays (#595) by @nikophil +* d1509fb fix(2.x): support readonly entities (#599) by @nikophil +* 2c8e048 feat(2.x): allow to configure default namespace fo make:factory (#600) by @nikophil +* 9174dc6 fix: restore PHPUnit error handler (#587) by @nikophil +* 4156302 tests: asserts story works without persistence (#589) by @nikophil +* ec2c895 minor: add phpunit attributes (#576) by @nikophil +* 90cc839 feat(sequence): sequence attributes should be compatible with 1.x (#575) by @nikophil +* 60ec275 fix: sqlite with orm v2 (#574) by @kbond +* ae82186 feat: compatibility with ORM v3 (#572) by @nikophil, @kbond +* 624e8d2 feat: foundry 2.0 πŸŽ‰ by @nikophil +* e74f6b9 fix(rector) second argument for many() is optional (#515) by @nikophil +* a555474 fix(rector): repository method is static (#515) by @nikophil +* 53f25a2 rector: rewrite phpdoc (#571) by @nikophil +* ecbc615 refactor: Foundry 2 BC layer (#515) by @nikophil From f046a5926dff04990c1dcdc006c2d7245827c542 Mon Sep 17 00:00:00 2001 From: Kevin Bond Date: Wed, 23 Oct 2024 15:37:17 -0400 Subject: [PATCH 002/102] changelog: update [skip ci] --- CHANGELOG.md | 90 ++-------------------------------------------------- 1 file changed, 3 insertions(+), 87 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7775670bc..06debdd25 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,49 +34,11 @@ July 12th, 2024 - [v2.0.6...v2.0.7](https://github.com/zenstruck/foundry/compare ## [v2.0.6](https://github.com/zenstruck/foundry/releases/tag/v2.0.6) -July 4th, 2024 - [v1.38.3...v2.0.6](https://github.com/zenstruck/foundry/compare/v1.38.3...v2.0.6) +July 4th, 2024 - [v2.0.5...v2.0.6](https://github.com/zenstruck/foundry/compare/v2.0.5...v2.0.6) * 52ca7b7 fix: only restore error handler for PHPUnit 10 or superior (#668) by @nikophil * b5090aa docs: Fix broken link to Without Persisting (#660) by @simoheinonen * 35b0404 feat: re-add Proxy assertions (#663) by @nikophil -* 6105a36 fix: make proxy work with last symfony/var-exporter version (#664) by @nikophil -* e8623a3 [DOC] Fix Upgrade Guide URL Rendering (#654) by @cezarpopa -* f7f133a fix: create ArrayCollection if needed (#645) by @nikophil -* 779bee4 fix: after_flush() can use objects created in global state (#653) by @nikophil -* 72e48bf tests(ci): add test permutation for PHPUnit >= 10 (#647) by @nikophil -* 1edf948 docs: fix incoherence (#652) by @nikophil -* 1c66e39 minor: improve repository assertion messages (#651) by @nikophil -* 0989c5d fix: don't try to proxify objects that are not persistable (#646) by @nikophil -* 50ae3dc fix: handle contravariance problem when proxifying class with unserialize method (#644) by @nikophil -* 6f0835f fix(2.x): only reset error handler in before class hook (#643) by @nikophil -* 3c31193 test: add test with multiple ORM schemas (#629) by @vincentchalamon -* 303211a fix: unproxy args in proxy objects (#635) by @nikophil -* b76c294 fix(2.x): support Symfony 7.1 (#622) by @nikophil -* 9cd97b7 docs: Improve DX for tests (#636) by @matthieumota -* 17b0228 fix(2.x): add back second parameter for after persist callbacks (#631) by @nikophil -* 0c7b3af docs: Fix typo in the upgrade guide (#624) by @stof -* 933ebbd docs: upgrade readme with a link to upgrade guide (#620) by @nikophil -* 5f0ce76 Fix `Instantiator::allowExtra` example (#616) by @norkunas -* c2cbcbc fix(orm): reset database instead of dropping the schema when using migrations (#615) by @vincentchalamon -* 1393c13 docs: update docs for foundry v2 (#613) by @nikophil -* 0034c78 feat(2.x, make:facotry): add phpdoc conditionnally with --with-phpdoc option (#611) by @nikophil -* a3ec473 fix: prevent refresh error with autorefresh (#610) by @nikophil -* ca8258a minor(make:factory): default false for ODM mapping (#605) by @nikophil -* df5bb6b minor(2.x): remove APP_ENV=test from phpunit.xml (#603) by @nikophil -* 0479ddf fix(2.x): allow sequence with associative arrays (#595) by @nikophil -* d1509fb fix(2.x): support readonly entities (#599) by @nikophil -* 2c8e048 feat(2.x): allow to configure default namespace fo make:factory (#600) by @nikophil -* 9174dc6 fix: restore PHPUnit error handler (#587) by @nikophil -* 4156302 tests: asserts story works without persistence (#589) by @nikophil -* ec2c895 minor: add phpunit attributes (#576) by @nikophil -* 90cc839 feat(sequence): sequence attributes should be compatible with 1.x (#575) by @nikophil -* 60ec275 fix: sqlite with orm v2 (#574) by @kbond -* ae82186 feat: compatibility with ORM v3 (#572) by @nikophil, @kbond -* 624e8d2 feat: foundry 2.0 πŸŽ‰ by @nikophil -* e74f6b9 fix(rector) second argument for many() is optional (#515) by @nikophil -* a555474 fix(rector): repository method is static (#515) by @nikophil -* 53f25a2 rector: rewrite phpdoc (#571) by @nikophil -* ecbc615 refactor: Foundry 2 BC layer (#515) by @nikophil ## [v2.0.5](https://github.com/zenstruck/foundry/releases/tag/v2.0.5) @@ -99,37 +61,11 @@ June 20th, 2024 - [v2.0.3...v2.0.4](https://github.com/zenstruck/foundry/compare ## [v2.0.3](https://github.com/zenstruck/foundry/releases/tag/v2.0.3) -June 19th, 2024 - [v1.38.2...v2.0.3](https://github.com/zenstruck/foundry/compare/v1.38.2...v2.0.3) +June 19th, 2024 - [v2.0.2...v2.0.3](https://github.com/zenstruck/foundry/compare/v2.0.2...v2.0.3) * 6f0835f fix(2.x): only reset error handler in before class hook (#643) by @nikophil * 3c31193 test: add test with multiple ORM schemas (#629) by @vincentchalamon * 303211a fix: unproxy args in proxy objects (#635) by @nikophil -* b76c294 fix(2.x): support Symfony 7.1 (#622) by @nikophil -* 9cd97b7 docs: Improve DX for tests (#636) by @matthieumota -* 17b0228 fix(2.x): add back second parameter for after persist callbacks (#631) by @nikophil -* 0c7b3af docs: Fix typo in the upgrade guide (#624) by @stof -* 933ebbd docs: upgrade readme with a link to upgrade guide (#620) by @nikophil -* 5f0ce76 Fix `Instantiator::allowExtra` example (#616) by @norkunas -* c2cbcbc fix(orm): reset database instead of dropping the schema when using migrations (#615) by @vincentchalamon -* 1393c13 docs: update docs for foundry v2 (#613) by @nikophil -* 0034c78 feat(2.x, make:facotry): add phpdoc conditionnally with --with-phpdoc option (#611) by @nikophil -* a3ec473 fix: prevent refresh error with autorefresh (#610) by @nikophil -* ca8258a minor(make:factory): default false for ODM mapping (#605) by @nikophil -* df5bb6b minor(2.x): remove APP_ENV=test from phpunit.xml (#603) by @nikophil -* 0479ddf fix(2.x): allow sequence with associative arrays (#595) by @nikophil -* d1509fb fix(2.x): support readonly entities (#599) by @nikophil -* 2c8e048 feat(2.x): allow to configure default namespace fo make:factory (#600) by @nikophil -* 9174dc6 fix: restore PHPUnit error handler (#587) by @nikophil -* 4156302 tests: asserts story works without persistence (#589) by @nikophil -* ec2c895 minor: add phpunit attributes (#576) by @nikophil -* 90cc839 feat(sequence): sequence attributes should be compatible with 1.x (#575) by @nikophil -* 60ec275 fix: sqlite with orm v2 (#574) by @kbond -* ae82186 feat: compatibility with ORM v3 (#572) by @nikophil, @kbond -* 624e8d2 feat: foundry 2.0 πŸŽ‰ by @nikophil -* e74f6b9 fix(rector) second argument for many() is optional (#515) by @nikophil -* a555474 fix(rector): repository method is static (#515) by @nikophil -* 53f25a2 rector: rewrite phpdoc (#571) by @nikophil -* ecbc615 refactor: Foundry 2 BC layer (#515) by @nikophil ## [v2.0.2](https://github.com/zenstruck/foundry/releases/tag/v2.0.2) @@ -150,24 +86,4 @@ June 10th, 2024 - [v2.0.0...v2.0.1](https://github.com/zenstruck/foundry/compare ## [v2.0.0](https://github.com/zenstruck/foundry/releases/tag/v2.0.0) -June 7th, 2024 - [v1.38.0...v2.0.0](https://github.com/zenstruck/foundry/compare/v1.38.0...v2.0.0) - -* 1393c13 docs: update docs for foundry v2 (#613) by @nikophil -* 0034c78 feat(2.x, make:facotry): add phpdoc conditionnally with --with-phpdoc option (#611) by @nikophil -* a3ec473 fix: prevent refresh error with autorefresh (#610) by @nikophil -* ca8258a minor(make:factory): default false for ODM mapping (#605) by @nikophil -* df5bb6b minor(2.x): remove APP_ENV=test from phpunit.xml (#603) by @nikophil -* 0479ddf fix(2.x): allow sequence with associative arrays (#595) by @nikophil -* d1509fb fix(2.x): support readonly entities (#599) by @nikophil -* 2c8e048 feat(2.x): allow to configure default namespace fo make:factory (#600) by @nikophil -* 9174dc6 fix: restore PHPUnit error handler (#587) by @nikophil -* 4156302 tests: asserts story works without persistence (#589) by @nikophil -* ec2c895 minor: add phpunit attributes (#576) by @nikophil -* 90cc839 feat(sequence): sequence attributes should be compatible with 1.x (#575) by @nikophil -* 60ec275 fix: sqlite with orm v2 (#574) by @kbond -* ae82186 feat: compatibility with ORM v3 (#572) by @nikophil, @kbond -* 624e8d2 feat: foundry 2.0 πŸŽ‰ by @nikophil -* e74f6b9 fix(rector) second argument for many() is optional (#515) by @nikophil -* a555474 fix(rector): repository method is static (#515) by @nikophil -* 53f25a2 rector: rewrite phpdoc (#571) by @nikophil -* ecbc615 refactor: Foundry 2 BC layer (#515) by @nikophil +June 7th, 2024 - _[Initial Release](https://github.com/zenstruck/foundry/commits/v2.0.0)_ From 2014ed908a28b3cf0929ccd7fd05cee0f533e08a Mon Sep 17 00:00:00 2001 From: Nicolas PHILIPPE Date: Thu, 24 Oct 2024 11:57:15 +0200 Subject: [PATCH 003/102] feature: allow to use `Factory::create()` and factory service in data providers (#648) --- .env | 1 + .github/workflows/ci.yml | 25 ++- phpstan.neon | 4 + phpunit | 52 +++-- phpunit-10.xml.dist | 2 +- phpunit.xml.dist | 2 +- src/Configuration.php | 18 +- src/Exception/FoundryNotBooted.php | 4 + .../BootFoundryOnDataProviderMethodCalled.php | 30 +++ src/PHPUnit/FoundryExtension.php | 50 +++++ ...ownFoundryOnDataProviderMethodFinished.php | 30 +++ src/Persistence/IsProxy.php | 6 +- src/Persistence/PersistentObjectFactory.php | 24 +++ .../PersistentProxyObjectFactory.php | 9 +- src/Persistence/Proxy.php | 5 + src/Persistence/ProxyGenerator.php | 38 +++- src/Persistence/functions.php | 12 ++ src/Test/Factories.php | 90 +++++++- tests/Fixture/SomeEnum.php | 9 + ...rForServiceFactoryInKernelTestCaseTest.php | 56 +++++ .../DataProvider/DataProviderInUnitTest.php | 68 ++++++ ...ithNonProxyFactoryInKernelTestCaseTest.php | 54 +++++ ...oviderWithProxyFactoryInKernelTestCase.php | 95 +++++++++ .../GenericDocumentProxyFactoryTest.php | 34 +++ .../GenericEntityProxyFactoryTest.php | 34 +++ .../Mongo/GenericDocumentFactoryTest.php | 3 +- .../Mongo/GenericDocumentProxyFactoryTest.php | 2 +- .../ORM/GenericEntityFactoryTest.php | 3 +- .../ORM/GenericEntityProxyFactoryTest.php | 2 +- .../Persistence/GenericFactoryTestCase.php | 200 +++++++++--------- .../GenericProxyFactoryTestCase.php | 78 +++---- 31 files changed, 851 insertions(+), 189 deletions(-) create mode 100644 src/PHPUnit/BootFoundryOnDataProviderMethodCalled.php create mode 100644 src/PHPUnit/FoundryExtension.php create mode 100644 src/PHPUnit/ShutdownFoundryOnDataProviderMethodFinished.php create mode 100644 tests/Integration/DataProvider/DataProviderForServiceFactoryInKernelTestCaseTest.php create mode 100644 tests/Integration/DataProvider/DataProviderInUnitTest.php create mode 100644 tests/Integration/DataProvider/DataProviderWithNonProxyFactoryInKernelTestCaseTest.php create mode 100644 tests/Integration/DataProvider/DataProviderWithProxyFactoryInKernelTestCase.php create mode 100644 tests/Integration/DataProvider/GenericDocumentProxyFactoryTest.php create mode 100644 tests/Integration/DataProvider/GenericEntityProxyFactoryTest.php diff --git a/.env b/.env index da52e373d..d443158a7 100644 --- a/.env +++ b/.env @@ -2,4 +2,5 @@ DATABASE_URL="mysql://root:1234@127.0.0.1:3307/foundry_test?serverVersion=5.7.42 MONGO_URL="mongodb://127.0.0.1:27018/dbName?compressors=disabled&gssapiServiceName=mongodb" DATABASE_RESET_MODE="schema" USE_DAMA_DOCTRINE_TEST_BUNDLE="0" +USE_FOUNDRY_PHPUNIT_EXTENSION="0" PHPUNIT_VERSION="9" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0d5bced4d..a29b02801 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,7 +8,7 @@ on: jobs: tests: - name: P:${{ matrix.php }}, S:${{ matrix.symfony }}, D:${{ matrix.database }}, PU:${{ matrix.phpunit }}${{ matrix.deps == 'lowest' && ' (lowest)' || '' }}${{ matrix.use-dama == 1 && contains(matrix.database, 'sql') && ' (dama)' || '' }}${{ !contains(matrix.database, 'sql') && '' || matrix.use-migrate == 1 && ' (migrate)' || ' (schema)' }} + name: P:${{ matrix.php }}, S:${{ matrix.symfony }}, D:${{ matrix.database }}, PU:${{ matrix.phpunit }}${{ matrix.deps == 'lowest' && ' (lowest)' || '' }}${{ matrix.use-dama == 1 && contains(matrix.database, 'sql') && ' (dama)' || '' }}${{ !contains(matrix.database, 'sql') && '' || matrix.use-migrate == 1 && ' (migrate)' || ' (schema)' }}${{ matrix.use-phpunit-extension == 1 && ' (phpunit extension)' || '' }} runs-on: ubuntu-latest strategy: fail-fast: false @@ -19,6 +19,7 @@ jobs: database: [ mysql, mongo ] use-dama: [ 1 ] use-migrate: [ 0 ] + use-phpunit-extension: [ 0 ] phpunit: [ 9 ] exclude: - php: 8.1 @@ -32,6 +33,7 @@ jobs: database: none use-dama: 1 use-migrate: 0 + use-phpunit-extension: 0 phpunit: 9 - php: 8.3 deps: highest @@ -39,6 +41,7 @@ jobs: database: mysql|mongo use-dama: 1 use-migrate: 0 + use-phpunit-extension: 0 phpunit: 9 - php: 8.3 deps: highest @@ -46,6 +49,7 @@ jobs: database: pgsql|mongo use-dama: 1 use-migrate: 0 + use-phpunit-extension: 0 phpunit: 9 - php: 8.3 deps: highest @@ -53,6 +57,7 @@ jobs: database: pgsql use-dama: 0 use-migrate: 0 + use-phpunit-extension: 0 phpunit: 9 - php: 8.3 deps: highest @@ -60,6 +65,7 @@ jobs: database: sqlite use-dama: 0 use-migrate: 0 + use-phpunit-extension: 0 phpunit: 9 - php: 8.3 deps: lowest @@ -67,6 +73,7 @@ jobs: database: sqlite use-dama: 0 use-migrate: 0 + use-phpunit-extension: 0 phpunit: 9 - php: 8.3 deps: lowest @@ -74,6 +81,7 @@ jobs: database: mysql use-dama: 1 use-migrate: 0 + use-phpunit-extension: 0 phpunit: 9 - php: 8.3 deps: highest @@ -81,6 +89,7 @@ jobs: database: mysql use-dama: 1 use-migrate: 1 + use-phpunit-extension: 0 phpunit: 9 - php: 8.3 deps: highest @@ -88,6 +97,7 @@ jobs: database: mysql|mongo use-dama: 1 use-migrate: 0 + use-phpunit-extension: 0 phpunit: 10 - php: 8.3 deps: highest @@ -95,11 +105,21 @@ jobs: database: mysql|mongo use-dama: 1 use-migrate: 0 + use-phpunit-extension: 0 + phpunit: 11 + - php: 8.3 + deps: highest + symfony: '*' + database: mysql|mongo + use-dama: 1 + use-migrate: 0 + use-phpunit-extension: 1 phpunit: 11 env: DATABASE_URL: ${{ contains(matrix.database, 'mysql') && 'mysql://root:root@localhost:3306/foundry?serverVersion=5.7.42' || contains(matrix.database, 'pgsql') && 'postgresql://root:root@localhost:5432/foundry?serverVersion=15' || contains(matrix.database, 'sqlite') && 'sqlite:///%kernel.project_dir%/var/data.db' || '' }} MONGO_URL: ${{ contains(matrix.database, 'mongo') && 'mongodb://127.0.0.1:27017/dbName?compressors=disabled&gssapiServiceName=mongodb' || '' }} USE_DAMA_DOCTRINE_TEST_BUNDLE: ${{ matrix.use-dama == 1 && contains(matrix.database, 'sql') && 1 || 0 }} + USE_FOUNDRY_PHPUNIT_EXTENSION: ${{ matrix.use-phpunit-extension }} PHPUNIT_VERSION: ${{ matrix.phpunit }} services: postgres: @@ -155,7 +175,8 @@ jobs: DATABASE_URL: postgresql://root:root@localhost:5432/foundry?serverVersion=15 MONGO_URL: mongodb://127.0.0.1:27017/dbName?compressors=disabled&gssapiServiceName=mongodb USE_DAMA_DOCTRINE_TEST_BUNDLE: 1 - PHPUNIT_VERSION: 9 + USE_FOUNDRY_PHPUNIT_EXTENSION: 1 + PHPUNIT_VERSION: 11 services: mongo: image: mongo:4 diff --git a/phpstan.neon b/phpstan.neon index f3e6e4d76..5f677458e 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -14,6 +14,10 @@ parameters: - identifier: missingType.iterableValue 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#' + path: src/Test/Factories.php + excludePaths: - tests/Fixture/Maker/expected/can_create_factory_with_auto_activated_not_persisted_option.php - tests/Fixture/Maker/expected/can_create_factory_interactively.php diff --git a/phpunit b/phpunit index 7de0be16c..60c41819a 100755 --- a/phpunit +++ b/phpunit @@ -8,9 +8,7 @@ check_phpunit_version() { REQUIRED_PHPUNIT_VERSION="${1?}" - if [[ "${INSTALLED_PHPUNIT_VERSION}" == *"dev"* ]] && [ "${REQUIRED_PHPUNIT_VERSION}" != "11.4" ]; then - echo 0; - elif [[ "${INSTALLED_PHPUNIT_VERSION}" == "${REQUIRED_PHPUNIT_VERSION}"* ]]; then + if [[ "${INSTALLED_PHPUNIT_VERSION}" == "${REQUIRED_PHPUNIT_VERSION}"* ]]; then echo 1; else echo 0; @@ -28,8 +26,8 @@ fi ### << ### >> update PHPUnit if needed -if [[ " 9 10 11 11.4 " != *" ${PHPUNIT_VERSION-9} "* ]]; then - echo "❌ PHPUNIT_VERSION should be one of 9, 10, 11, 11.4"; +if [[ " 9 10 11 " != *" ${PHPUNIT_VERSION-9} "* ]]; then + echo "❌ PHPUNIT_VERSION should be one of 9, 10, 11"; exit 1; fi @@ -37,33 +35,25 @@ SHOULD_UPDATE_PHPUNIT=$(check_phpunit_version "${PHPUNIT_VERSION}") if [ "${SHOULD_UPDATE_PHPUNIT}" = "0" ]; then echo "ℹ️ Upgrading PHPUnit to ${PHPUNIT_VERSION}" - if [ "${PHPUNIT_VERSION}" = "9" ]; then - composer update phpunit/phpunit:^9 -W --dev - else - if [ "${PHPUNIT_VERSION}" = "11.4" ]; then - composer update phpunit/phpunit:11.4.x-dev -W --dev - else - composer update "phpunit/phpunit:^${PHPUNIT_VERSION}" -W --dev - fi - fi + composer update "phpunit/phpunit:^${PHPUNIT_VERSION}" -W fi ### << -### >> guess extensions -EXTENSION="" +### >> actually execute PHPUnit with the right options +DAMA_EXTENSION="DAMA\DoctrineTestBundle\PHPUnit\PHPUnitExtension" +FOUNDRY_EXTENSION="Zenstruck\Foundry\PHPUnit\FoundryExtension" -if [ "${USE_DAMA_DOCTRINE_TEST_BUNDLE:-0}" = "1" ]; then - EXTENSION="DAMA\DoctrineTestBundle\PHPUnit\PHPUnitExtension" +if [ "${USE_FOUNDRY_PHPUNIT_EXTENSION:-0}" = "1" ] && [ "${PHPUNIT_VERSION}" != "11" ]; then + echo "❌ USE_FOUNDRY_PHPUNIT_EXTENSION could only be used with PHPUNIT_VERSION=11"; + exit 1; fi -### << -### >> actually execute PHPUnit with the right options case ${PHPUNIT_VERSION} in "9") - if [ -z "${EXTENSION}" ]; then - vendor/bin/phpunit -c phpunit.xml.dist "$@" + if [ "${USE_DAMA_DOCTRINE_TEST_BUNDLE:-0}" = "1" ]; then + vendor/bin/phpunit -c phpunit.xml.dist --extensions "${DAMA_EXTENSION}" "$@" else - vendor/bin/phpunit -c phpunit.xml.dist --extensions "${EXTENSION}" "$@" + vendor/bin/phpunit -c phpunit.xml.dist "$@" fi ;; @@ -72,12 +62,18 @@ case ${PHPUNIT_VERSION} in vendor/bin/phpunit -c phpunit-10.xml.dist "$@" ;; - "11"|"11.4") - if [ -z "${EXTENSION}" ]; then - vendor/bin/phpunit -c phpunit-10.xml.dist "$@" - else - vendor/bin/phpunit -c phpunit-10.xml.dist --extension "${EXTENSION}" "$@" + "11") + PHPUNIT_EXEC="vendor/bin/phpunit -c phpunit-10.xml.dist $@" + if [ "${USE_DAMA_DOCTRINE_TEST_BUNDLE:-0}" = "1" ]; then + PHPUNIT_EXEC="${PHPUNIT_EXEC} --extension "${DAMA_EXTENSION}"" fi + + if [ "${USE_FOUNDRY_PHPUNIT_EXTENSION:-0}" = "1" ]; then + PHPUNIT_EXEC="${PHPUNIT_EXEC} --extension "${FOUNDRY_EXTENSION}"" + fi + + echo $PHPUNIT_EXEC + $PHPUNIT_EXEC ;; esac ### << diff --git a/phpunit-10.xml.dist b/phpunit-10.xml.dist index 1727f9b8b..21bfa9c48 100644 --- a/phpunit-10.xml.dist +++ b/phpunit-10.xml.dist @@ -1,7 +1,7 @@ bootedForDataProvider; + } + public static function instance(): self { if (!self::$instance) { - throw new FoundryNotBooted('Foundry is not yet booted. Ensure ZenstruckFoundryBundle is enabled. If in a test, ensure your TestCase has the Factories trait.'); + throw new FoundryNotBooted(); } return \is_callable(self::$instance) ? (self::$instance)() : self::$instance; @@ -88,6 +98,12 @@ public static function boot(\Closure|self $configuration): void self::$instance = $configuration; } + public static function bootForDataProvider(\Closure|self $configuration): void + { + self::$instance = \is_callable($configuration) ? ($configuration)() : $configuration; + self::$instance->bootedForDataProvider = true; + } + public static function shutdown(): void { StoryRegistry::reset(); diff --git a/src/Exception/FoundryNotBooted.php b/src/Exception/FoundryNotBooted.php index e4b04e697..5f7fbef27 100644 --- a/src/Exception/FoundryNotBooted.php +++ b/src/Exception/FoundryNotBooted.php @@ -16,4 +16,8 @@ */ final class FoundryNotBooted extends \LogicException { + public function __construct() + { + parent::__construct('Foundry is not yet booted. Ensure ZenstruckFoundryBundle is enabled. If in a test, ensure your TestCase has the Factories trait.'); + } } diff --git a/src/PHPUnit/BootFoundryOnDataProviderMethodCalled.php b/src/PHPUnit/BootFoundryOnDataProviderMethodCalled.php new file mode 100644 index 000000000..d9035fc91 --- /dev/null +++ b/src/PHPUnit/BootFoundryOnDataProviderMethodCalled.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\PHPUnit; + +use PHPUnit\Event; + +/** + * @internal + * @author Nicolas PHILIPPE + */ +final class BootFoundryOnDataProviderMethodCalled implements Event\Test\DataProviderMethodCalledSubscriber +{ + public function notify(Event\Test\DataProviderMethodCalled $event): void + { + if (method_exists($event->testMethod()->className(), '_bootForDataProvider')) { + call_user_func([$event->testMethod()->className(), '_bootForDataProvider']); + } + } +} diff --git a/src/PHPUnit/FoundryExtension.php b/src/PHPUnit/FoundryExtension.php new file mode 100644 index 000000000..c6ac7ddd0 --- /dev/null +++ b/src/PHPUnit/FoundryExtension.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\PHPUnit; + +use PHPUnit\Metadata\Version\ConstraintRequirement; +use PHPUnit\Runner; +use PHPUnit\TextUI; +use Zenstruck\Foundry\Configuration; + +/** + * @internal + * @author Nicolas PHILIPPE + */ +final class FoundryExtension implements Runner\Extension\Extension +{ + public const MIN_PHPUNIT_VERSION = '11.4'; + + public function bootstrap( + TextUI\Configuration\Configuration $configuration, + Runner\Extension\Facade $facade, + Runner\Extension\ParameterCollection $parameters, + ): void { + if (!ConstraintRequirement::from(self::MIN_PHPUNIT_VERSION)->isSatisfiedBy(Runner\Version::id())) { + throw new \LogicException( + \sprintf('Your PHPUnit version (%s) is not compatible with the minimum version (%s) needed to use this extension.', Runner\Version::id(), self::MIN_PHPUNIT_VERSION) + ); + } + + // shutdown Foundry if for some reason it has been booted before + if (Configuration::isBooted()) { + Configuration::shutdown(); + } + + $facade->registerSubscribers( + new BootFoundryOnDataProviderMethodCalled(), + new ShutdownFoundryOnDataProviderMethodFinished(), + ); + } +} diff --git a/src/PHPUnit/ShutdownFoundryOnDataProviderMethodFinished.php b/src/PHPUnit/ShutdownFoundryOnDataProviderMethodFinished.php new file mode 100644 index 000000000..57e34daa8 --- /dev/null +++ b/src/PHPUnit/ShutdownFoundryOnDataProviderMethodFinished.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\PHPUnit; + +use PHPUnit\Event; + +/** + * @internal + * @author Nicolas PHILIPPE + */ +final class ShutdownFoundryOnDataProviderMethodFinished implements Event\Test\DataProviderMethodFinishedSubscriber +{ + public function notify(Event\Test\DataProviderMethodFinished $event): void + { + if (method_exists($event->testMethod()->className(), '_shutdownAfterDataProvider')) { + call_user_func([$event->testMethod()->className(), '_shutdownAfterDataProvider']); + } + } +} diff --git a/src/Persistence/IsProxy.php b/src/Persistence/IsProxy.php index 044af5ac1..a49967f6b 100644 --- a/src/Persistence/IsProxy.php +++ b/src/Persistence/IsProxy.php @@ -11,7 +11,6 @@ namespace Zenstruck\Foundry\Persistence; -use Doctrine\ODM\MongoDB\DocumentManager; use Symfony\Component\VarExporter\LazyProxyTrait; use Zenstruck\Assert; use Zenstruck\Foundry\Configuration; @@ -128,6 +127,11 @@ public function _assertNotPersisted(string $message = '{entity} is persisted but return $this; } + public function _initializeLazyObject(): void + { + $this->initializeLazyObject(); + } + private function isPersisted(): bool { try { diff --git a/src/Persistence/PersistentObjectFactory.php b/src/Persistence/PersistentObjectFactory.php index e7ca86489..4198adddc 100644 --- a/src/Persistence/PersistentObjectFactory.php +++ b/src/Persistence/PersistentObjectFactory.php @@ -194,6 +194,8 @@ public function create(callable|array $attributes = []): object { $object = parent::create($attributes); + $this->throwIfCannotCreateObject(); + if (!$this->isPersisting()) { return $object; } @@ -329,4 +331,26 @@ final protected function isPersisting(): bool return $this->persist ?? $config->isPersistenceAvailable() && $config->persistence()->isEnabled() && $config->persistence()->autoPersist(static::class()); } + + private function throwIfCannotCreateObject(): void + { + $configuration = Configuration::instance(); + + /** + * "false === $configuration->inADataProvider()" would also mean that the PHPUnit extension is NOT used + * so a `FoundryNotBooted` exception would be thrown if we actually are in a data provider. + */ + if (!$configuration->inADataProvider()) { + return; + } + + if ( + !$configuration->isPersistenceAvailable() + || $this instanceof PersistentProxyObjectFactory + ) { + return; + } + + throw new \LogicException(\sprintf('Cannot create object in a data provider for non-proxy factories. Transform your factory into a "%s", or call "create()" method in the test. See https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#phpunit-data-providers', PersistentProxyObjectFactory::class)); + } } diff --git a/src/Persistence/PersistentProxyObjectFactory.php b/src/Persistence/PersistentProxyObjectFactory.php index a2e78d0a0..226e80459 100644 --- a/src/Persistence/PersistentProxyObjectFactory.php +++ b/src/Persistence/PersistentProxyObjectFactory.php @@ -31,8 +31,13 @@ abstract public static function class(): string; * @return T|Proxy * @phpstan-return T&Proxy */ - public function create(callable|array $attributes = []): object + final public function create(callable|array $attributes = []): object { + $configuration = Configuration::instance(); + if ($configuration->inADataProvider()) { + return ProxyGenerator::wrapFactory($this, $attributes); + } + return proxy(parent::create($attributes)); // @phpstan-ignore function.unresolvableReturnType } @@ -40,7 +45,7 @@ public function create(callable|array $attributes = []): object * @return T|Proxy * @phpstan-return T&Proxy */ - public static function createOne(array|callable $attributes = []): mixed + final public static function createOne(array|callable $attributes = []): mixed { return proxy(parent::createOne($attributes)); // @phpstan-ignore function.unresolvableReturnType } diff --git a/src/Persistence/Proxy.php b/src/Persistence/Proxy.php index eaa49192d..637e0e2ff 100644 --- a/src/Persistence/Proxy.php +++ b/src/Persistence/Proxy.php @@ -87,4 +87,9 @@ public function _assertNotPersisted(string $message = '{entity} is persisted but * @return ProxyRepositoryDecorator> */ public function _repository(): ProxyRepositoryDecorator; + + /** + * @internal + */ + public function _initializeLazyObject(): void; } diff --git a/src/Persistence/ProxyGenerator.php b/src/Persistence/ProxyGenerator.php index 27f5d9d2a..6b26f0a03 100644 --- a/src/Persistence/ProxyGenerator.php +++ b/src/Persistence/ProxyGenerator.php @@ -15,11 +15,14 @@ use Symfony\Component\VarExporter\LazyObjectInterface; use Symfony\Component\VarExporter\LazyProxyTrait; use Symfony\Component\VarExporter\ProxyHelper; +use Zenstruck\Foundry\Factory; /** * @author Kevin Bond * * @internal + * + * @phpstan-import-type Attributes from Factory */ final class ProxyGenerator { @@ -43,6 +46,19 @@ public static function wrap(object $object): Proxy return self::generateClassFor($object)::createLazyProxy(static fn() => $object); // @phpstan-ignore staticMethod.unresolvableReturnType } + /** + * @template T of object + * + * @param PersistentProxyObjectFactory $factory + * @phpstan-param Attributes $attributes + * + * @return T&Proxy + */ + public static function wrapFactory(PersistentProxyObjectFactory $factory, callable|array $attributes): Proxy + { + return self::generateClassFor($factory)::createLazyProxy(static fn() => unproxy($factory->create($attributes))); // @phpstan-ignore-line + } + /** * @template T * @@ -67,6 +83,14 @@ public static function unwrap(mixed $what): mixed return $what; } + /** + * @param class-string $class + */ + public static function proxyClassNameFor(string $class): string + { + return \str_replace('\\', '', $class).'Proxy'; + } + /** * @template T of object * @@ -76,8 +100,8 @@ public static function unwrap(mixed $what): mixed */ private static function generateClassFor(object $object): string { - /** @var class-string $class */ - $class = $object instanceof DoctrineProxy ? \get_parent_class($object) : $object::class; + $class = self::extractClassName($object); + $proxyClass = self::proxyClassNameFor($class); /** @var class-string&T> $proxyClass */ @@ -102,10 +126,14 @@ private static function generateClassFor(object $object): string } /** - * @param class-string $class + * @return class-string */ - public static function proxyClassNameFor(string $class): string + private static function extractClassName(object $object): string { - return \str_replace('\\', '', $class).'Proxy'; + if ($object instanceof PersistentProxyObjectFactory) { + return $object::class(); + } + + return $object instanceof DoctrineProxy ? \get_parent_class($object) : $object::class; // @phpstan-ignore return.type } } diff --git a/src/Persistence/functions.php b/src/Persistence/functions.php index 99087678b..651ce0eaf 100644 --- a/src/Persistence/functions.php +++ b/src/Persistence/functions.php @@ -171,3 +171,15 @@ function enable_persisting(): void { Configuration::instance()->persistence()->enablePersisting(); } + +/** + * @internal + */ +function initialize_proxy_object(mixed $what): void +{ + match (true) { + $what instanceof Proxy => $what->_initializeLazyObject(), + \is_array($what) => \array_map(initialize_proxy_object(...), $what), + default => true, // do nothing + }; +} diff --git a/src/Test/Factories.php b/src/Test/Factories.php index e75ac193c..4f31432a6 100644 --- a/src/Test/Factories.php +++ b/src/Test/Factories.php @@ -16,6 +16,8 @@ use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; use Zenstruck\Foundry\Configuration; +use function Zenstruck\Foundry\Persistence\initialize_proxy_object; + /** * @author Kevin Bond */ @@ -26,7 +28,26 @@ trait Factories * @before */ #[Before] - public static function _bootFoundry(): void + public function _beforeHook(): void + { + $this->_bootFoundry(); + $this->_loadDataProvidedProxies(); + } + + /** + * @internal + * @after + */ + #[After] + public static function _shutdownFoundry(): void + { + Configuration::shutdown(); + } + + /** + * @internal + */ + private function _bootFoundry(): void { if (!\is_subclass_of(static::class, KernelTestCase::class)) { // @phpstan-ignore function.impossibleType // unit test @@ -46,12 +67,73 @@ public static function _bootFoundry(): void } /** + * If a persistent object has been created in a data provider, we need to initialize the proxy object, + * which will trigger the object to be persisted. + * + * Otherwise, such test would not pass: + * ```php + * #[DataProvider('provide')] + * public function testSomething(MyEntity $entity): void + * { + * MyEntityFactory::assert()->count(1); + * } + * + * public static function provide(): iterable + * { + * yield [MyEntityFactory::createOne()]; + * } + * ``` + * + * Sadly, this cannot be done in a subscriber, since PHPUnit does not give access to the actual tests instances. + * * @internal - * @after */ - #[After] - public static function _shutdownFoundry(): void + private function _loadDataProvidedProxies(): void { + if (!\is_subclass_of(static::class, KernelTestCase::class)) { // @phpstan-ignore function.impossibleType + return; + } + + $providedData = \method_exists($this, 'getProvidedData') ? $this->getProvidedData() : $this->providedData(); // @phpstan-ignore method.notFound + + initialize_proxy_object($providedData); + } + + /** + * @see \Zenstruck\Foundry\PHPUnit\BootFoundryOnDataProviderMethodCalled + * @internal + */ + public static function _bootForDataProvider(): void + { + if (!\is_subclass_of(static::class, KernelTestCase::class)) { // @phpstan-ignore function.impossibleType + // unit test + Configuration::bootForDataProvider(UnitTestConfig::build()); + + return; + } + + // integration test + Configuration::bootForDataProvider(static function() { + 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 + }); + } + + /** + * @internal + * @see \Zenstruck\Foundry\PHPUnit\ShutdownFoundryOnDataProviderMethodFinished + */ + public static function _shutdownAfterDataProvider(): void + { + if (\is_subclass_of(static::class, KernelTestCase::class)) { // @phpstan-ignore function.impossibleType + self::ensureKernelShutdown(); // @phpstan-ignore staticMethod.notFound + static::$class = null; // @phpstan-ignore staticProperty.notFound + static::$kernel = null; // @phpstan-ignore staticProperty.notFound + static::$booted = false; // @phpstan-ignore staticProperty.notFound + } Configuration::shutdown(); } } diff --git a/tests/Fixture/SomeEnum.php b/tests/Fixture/SomeEnum.php index f91d224c6..a874a6fbc 100644 --- a/tests/Fixture/SomeEnum.php +++ b/tests/Fixture/SomeEnum.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Zenstruck\Foundry\Tests\Fixture; enum SomeEnum diff --git a/tests/Integration/DataProvider/DataProviderForServiceFactoryInKernelTestCaseTest.php b/tests/Integration/DataProvider/DataProviderForServiceFactoryInKernelTestCaseTest.php new file mode 100644 index 000000000..4d16399c9 --- /dev/null +++ b/tests/Integration/DataProvider/DataProviderForServiceFactoryInKernelTestCaseTest.php @@ -0,0 +1,56 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Tests\Integration\DataProvider; + +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\RequiresPhpunit; +use PHPUnit\Framework\Attributes\RequiresPhpunitExtension; +use PHPUnit\Framework\Attributes\Test; +use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; +use Zenstruck\Foundry\Configuration; +use Zenstruck\Foundry\PHPUnit\FoundryExtension; +use Zenstruck\Foundry\Test\Factories; +use Zenstruck\Foundry\Tests\Fixture\Factories\Object1Factory; +use Zenstruck\Foundry\Tests\Fixture\Object1; + +/** + * @author Nicolas PHILIPPE + * @requires PHPUnit 11.4 + */ +#[RequiresPhpunit('11.4')] +#[RequiresPhpunitExtension(FoundryExtension::class)] +final class DataProviderForServiceFactoryInKernelTestCaseTest extends KernelTestCase +{ + use Factories; + + /** + * @test + */ + #[Test] + #[DataProvider('createObjectFromServiceFactoryInDataProvider')] + public function it_can_create_one_object_in_data_provider(?Object1 $providedData): void + { + self::assertFalse(Configuration::instance()->inADataProvider()); + + self::assertInstanceOf(Object1::class, $providedData); + $this->assertSame('router-constructor', $providedData->getProp1()); + } + + public static function createObjectFromServiceFactoryInDataProvider(): iterable + { + yield 'service factory' => [ + Object1Factory::createOne(), + ]; + } +} diff --git a/tests/Integration/DataProvider/DataProviderInUnitTest.php b/tests/Integration/DataProvider/DataProviderInUnitTest.php new file mode 100644 index 000000000..6eda443a7 --- /dev/null +++ b/tests/Integration/DataProvider/DataProviderInUnitTest.php @@ -0,0 +1,68 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Tests\Integration\DataProvider; + +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\RequiresPhpunit; +use PHPUnit\Framework\Attributes\RequiresPhpunitExtension; +use PHPUnit\Framework\Attributes\Test; +use PHPUnit\Framework\TestCase; +use Zenstruck\Foundry\PHPUnit\FoundryExtension; +use Zenstruck\Foundry\Test\Factories; +use Zenstruck\Foundry\Tests\Fixture\Entity\GenericEntity; +use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\GenericEntityFactory; +use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\GenericProxyEntityFactory; +use Zenstruck\Foundry\Tests\Fixture\Factories\Object1Factory; +use Zenstruck\Foundry\Tests\Fixture\Factories\Object2Factory; +use Zenstruck\Foundry\Tests\Fixture\Object1; +use Zenstruck\Foundry\Tests\Fixture\Object2; + +use function Zenstruck\Foundry\Persistence\unproxy; + +/** + * @author Nicolas PHILIPPE + * @requires PHPUnit 11.4 + */ +#[RequiresPhpunit('11.4')] +#[RequiresPhpunitExtension(FoundryExtension::class)] +final class DataProviderInUnitTest extends TestCase +{ + use Factories; + + #[Test] + #[DataProvider('createObjectWithObjectFactoryInDataProvider')] + public function assert_it_can_create_object_with_object_factory_in_data_provider(mixed $providedData, mixed $expectedData): void + { + self::assertEquals($expectedData, $providedData); + } + + public static function createObjectWithObjectFactoryInDataProvider(): iterable + { + yield 'object factory' => [Object2Factory::createOne(['object' => new Object1('prop1')]), new Object2(new Object1('prop1'))]; + yield 'service factory can be used if dependency is optional' => [Object1Factory::createOne(), new Object1('value1')]; + } + + #[Test] + #[DataProvider('createObjectWithPersistentObjectFactoryInDataProvider')] + public function assert_it_can_create_object_with_persistent_factory_in_data_provider(mixed $providedData, mixed $expectedData): void + { + self::assertEquals($expectedData, unproxy($providedData)); + } + + public static function createObjectWithPersistentObjectFactoryInDataProvider(): iterable + { + yield 'persistent factory' => [GenericEntityFactory::createOne(), new GenericEntity('default1')]; + yield 'proxy persistent factory' => [GenericProxyEntityFactory::createOne(), new GenericEntity('default1')]; + } +} diff --git a/tests/Integration/DataProvider/DataProviderWithNonProxyFactoryInKernelTestCaseTest.php b/tests/Integration/DataProvider/DataProviderWithNonProxyFactoryInKernelTestCaseTest.php new file mode 100644 index 000000000..50d9e2118 --- /dev/null +++ b/tests/Integration/DataProvider/DataProviderWithNonProxyFactoryInKernelTestCaseTest.php @@ -0,0 +1,54 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Tests\Integration\DataProvider; + +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\RequiresPhpunit; +use PHPUnit\Framework\Attributes\RequiresPhpunitExtension; +use PHPUnit\Framework\Attributes\Test; +use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; +use Zenstruck\Foundry\PHPUnit\FoundryExtension; +use Zenstruck\Foundry\Test\Factories; +use Zenstruck\Foundry\Test\ResetDatabase; +use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\GenericEntityFactory; + +/** + * @author Nicolas PHILIPPE + * @requires PHPUnit 11.4 + */ +#[RequiresPhpunit('11.4')] +#[RequiresPhpunitExtension(FoundryExtension::class)] +final class DataProviderWithNonProxyFactoryInKernelTestCaseTest extends KernelTestCase +{ + use Factories; + use ResetDatabase; + + #[Test] + #[DataProvider('throwsExceptionWhenCreatingObjectInDataProvider')] + public function it_throws_an_exception_when_trying_to_create_an_object_in_data_provider(?\Throwable $e): void + { + self::assertInstanceOf(\LogicException::class, $e); + self::assertStringStartsWith('Cannot create object in a data provider for non-proxy factories.', $e->getMessage()); + } + + public static function throwsExceptionWhenCreatingObjectInDataProvider(): iterable + { + try { + GenericEntityFactory::createOne(); + } catch (\Throwable $e) { + } + + yield [$e ?? null]; + } +} diff --git a/tests/Integration/DataProvider/DataProviderWithProxyFactoryInKernelTestCase.php b/tests/Integration/DataProvider/DataProviderWithProxyFactoryInKernelTestCase.php new file mode 100644 index 000000000..a502aad19 --- /dev/null +++ b/tests/Integration/DataProvider/DataProviderWithProxyFactoryInKernelTestCase.php @@ -0,0 +1,95 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Tests\Integration\DataProvider; + +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\RequiresPhpunit; +use PHPUnit\Framework\Attributes\RequiresPhpunitExtension; +use PHPUnit\Framework\Attributes\Test; +use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; +use Zenstruck\Foundry\Persistence\PersistentProxyObjectFactory; +use Zenstruck\Foundry\Persistence\Proxy; +use Zenstruck\Foundry\PHPUnit\FoundryExtension; +use Zenstruck\Foundry\Test\Factories; +use Zenstruck\Foundry\Test\ResetDatabase; +use Zenstruck\Foundry\Tests\Fixture\Model\GenericModel; + +use function Zenstruck\Foundry\Persistence\unproxy; + +/** + * @author Nicolas PHILIPPE + * @requires PHPUnit 11.4 + */ +#[RequiresPhpunit('11.4')] +#[RequiresPhpunitExtension(FoundryExtension::class)] +abstract class DataProviderWithProxyFactoryInKernelTestCase extends KernelTestCase +{ + use Factories; + use ResetDatabase; + + #[Test] + #[DataProvider('createOneObjectInDataProvider')] + public function assert_it_can_create_one_object_in_data_provider(?GenericModel $providedData): void + { + static::factory()::assert()->count(1); + + self::assertInstanceOf(Proxy::class, $providedData); + self::assertNotInstanceOf(Proxy::class, unproxy($providedData)); // asserts two proxies are not nested + self::assertInstanceOf(GenericModel::class, $providedData); + self::assertSame('value set in data provider', $providedData->getProp1()); + } + + public static function createOneObjectInDataProvider(): iterable + { + yield 'createOne()' => [ + static::factory()::createOne(['prop1' => 'value set in data provider']), + ]; + + yield 'create()' => [ + static::factory()->create(['prop1' => 'value set in data provider']), + ]; + } + + #[Test] + #[DataProvider('createMultipleObjectsInDataProvider')] + public function assert_it_can_create_multiple_objects_in_data_provider(?array $providedData): void + { + self::assertIsArray($providedData); + static::factory()::assert()->count(2); + self::assertSame('prop 1', $providedData[0]->getProp1()); + self::assertSame('prop 2', $providedData[1]->getProp1()); + } + + public static function createMultipleObjectsInDataProvider(): iterable + { + yield 'createSequence()' => [ + static::factory()::createSequence([ + ['prop1' => 'prop 1'], + ['prop1' => 'prop 2'], + ]), + ]; + + yield 'FactoryCollection::create()' => [ + static::factory()->sequence([ + ['prop1' => 'prop 1'], + ['prop1' => 'prop 2'], + ])->create(), + ]; + } + + /** + * @return PersistentProxyObjectFactory + */ + abstract protected static function factory(): PersistentProxyObjectFactory; +} diff --git a/tests/Integration/DataProvider/GenericDocumentProxyFactoryTest.php b/tests/Integration/DataProvider/GenericDocumentProxyFactoryTest.php new file mode 100644 index 000000000..ae69e50f2 --- /dev/null +++ b/tests/Integration/DataProvider/GenericDocumentProxyFactoryTest.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Tests\Integration\DataProvider; + +use PHPUnit\Framework\Attributes\RequiresPhpunit; +use PHPUnit\Framework\Attributes\RequiresPhpunitExtension; +use Zenstruck\Foundry\PHPUnit\FoundryExtension; +use Zenstruck\Foundry\Tests\Fixture\Factories\Document\GenericProxyDocumentFactory; +use Zenstruck\Foundry\Tests\Integration\RequiresMongo; + +/** + * @author Nicolas PHILIPPE + * @requires PHPUnit 11.4 + */ +#[RequiresPhpunit('11.4')] +#[RequiresPhpunitExtension(FoundryExtension::class)] +final class GenericDocumentProxyFactoryTest extends DataProviderWithProxyFactoryInKernelTestCase +{ + use RequiresMongo; + + protected static function factory(): GenericProxyDocumentFactory + { + return GenericProxyDocumentFactory::new(); + } +} diff --git a/tests/Integration/DataProvider/GenericEntityProxyFactoryTest.php b/tests/Integration/DataProvider/GenericEntityProxyFactoryTest.php new file mode 100644 index 000000000..59d1488c6 --- /dev/null +++ b/tests/Integration/DataProvider/GenericEntityProxyFactoryTest.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Tests\Integration\DataProvider; + +use PHPUnit\Framework\Attributes\RequiresPhpunit; +use PHPUnit\Framework\Attributes\RequiresPhpunitExtension; +use Zenstruck\Foundry\PHPUnit\FoundryExtension; +use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\GenericProxyEntityFactory; +use Zenstruck\Foundry\Tests\Integration\RequiresORM; + +/** + * @author Nicolas PHILIPPE + * @requires PHPUnit 11.4 + */ +#[RequiresPhpunit('11.4')] +#[RequiresPhpunitExtension(FoundryExtension::class)] +final class GenericEntityProxyFactoryTest extends DataProviderWithProxyFactoryInKernelTestCase +{ + use RequiresORM; + + protected static function factory(): GenericProxyEntityFactory + { + return GenericProxyEntityFactory::new(); + } +} diff --git a/tests/Integration/Mongo/GenericDocumentFactoryTest.php b/tests/Integration/Mongo/GenericDocumentFactoryTest.php index ab6f5a606..df7be982f 100644 --- a/tests/Integration/Mongo/GenericDocumentFactoryTest.php +++ b/tests/Integration/Mongo/GenericDocumentFactoryTest.php @@ -12,7 +12,6 @@ namespace Zenstruck\Foundry\Tests\Integration\Mongo; use Zenstruck\Foundry\Tests\Fixture\Factories\Document\GenericDocumentFactory; -use Zenstruck\Foundry\Tests\Fixture\Factories\GenericModelFactory; use Zenstruck\Foundry\Tests\Integration\Persistence\GenericFactoryTestCase; use Zenstruck\Foundry\Tests\Integration\RequiresMongo; @@ -23,7 +22,7 @@ final class GenericDocumentFactoryTest extends GenericFactoryTestCase { use RequiresMongo; - protected function factory(): GenericModelFactory + protected static function factory(): GenericDocumentFactory { return GenericDocumentFactory::new(); } diff --git a/tests/Integration/Mongo/GenericDocumentProxyFactoryTest.php b/tests/Integration/Mongo/GenericDocumentProxyFactoryTest.php index f3e623657..05d2d9f85 100644 --- a/tests/Integration/Mongo/GenericDocumentProxyFactoryTest.php +++ b/tests/Integration/Mongo/GenericDocumentProxyFactoryTest.php @@ -26,7 +26,7 @@ final class GenericDocumentProxyFactoryTest extends GenericProxyFactoryTestCase { use RequiresMongo; - protected function factory(): PersistentProxyObjectFactory + protected static function factory(): GenericProxyDocumentFactory { return GenericProxyDocumentFactory::new(); } diff --git a/tests/Integration/ORM/GenericEntityFactoryTest.php b/tests/Integration/ORM/GenericEntityFactoryTest.php index 9b7458758..198e279ba 100644 --- a/tests/Integration/ORM/GenericEntityFactoryTest.php +++ b/tests/Integration/ORM/GenericEntityFactoryTest.php @@ -12,7 +12,6 @@ namespace Zenstruck\Foundry\Tests\Integration\ORM; use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\GenericEntityFactory; -use Zenstruck\Foundry\Tests\Fixture\Factories\GenericModelFactory; use Zenstruck\Foundry\Tests\Integration\Persistence\GenericFactoryTestCase; use Zenstruck\Foundry\Tests\Integration\RequiresORM; @@ -23,7 +22,7 @@ final class GenericEntityFactoryTest extends GenericFactoryTestCase { use RequiresORM; - protected function factory(): GenericModelFactory + protected static function factory(): GenericEntityFactory { return GenericEntityFactory::new(); } diff --git a/tests/Integration/ORM/GenericEntityProxyFactoryTest.php b/tests/Integration/ORM/GenericEntityProxyFactoryTest.php index 7cf8a82c9..8d0f47a39 100644 --- a/tests/Integration/ORM/GenericEntityProxyFactoryTest.php +++ b/tests/Integration/ORM/GenericEntityProxyFactoryTest.php @@ -26,7 +26,7 @@ final class GenericEntityProxyFactoryTest extends GenericProxyFactoryTestCase { use RequiresORM; - protected function factory(): PersistentProxyObjectFactory + protected static function factory(): GenericProxyEntityFactory { return GenericProxyEntityFactory::new(); } diff --git a/tests/Integration/Persistence/GenericFactoryTestCase.php b/tests/Integration/Persistence/GenericFactoryTestCase.php index af504f9cd..de4b4c2ef 100644 --- a/tests/Integration/Persistence/GenericFactoryTestCase.php +++ b/tests/Integration/Persistence/GenericFactoryTestCase.php @@ -43,27 +43,27 @@ abstract class GenericFactoryTestCase extends KernelTestCase */ public function can_create_and_update(): void { - $this->factory()::assert()->empty(); + static::factory()::assert()->empty(); - $object = $this->factory()->create(); + $object = static::factory()->create(); $this->assertNotNull($object->id); $this->assertSame('default1', $object->getProp1()); - $this->factory()::assert() + static::factory()::assert() ->count(1) ->exists(['prop1' => 'default1']) ->notExists(['prop1' => 'invalid']) ; - $this->assertSame($object->id, $this->factory()->first()->id); - $this->assertSame($object->id, $this->factory()->last()->id); + $this->assertSame($object->id, static::factory()->first()->id); + $this->assertSame($object->id, static::factory()->last()->id); $object->setProp1('new value'); save($object); $this->assertSame('new value', $object->getProp1()); - $this->factory()::assert()->exists(['prop1' => 'new value']); + static::factory()::assert()->exists(['prop1' => 'new value']); } /** @@ -71,18 +71,18 @@ public function can_create_and_update(): void */ public function can_disable_auto_persist(): void { - $this->factory()->repository()->assert()->empty(); + static::factory()->repository()->assert()->empty(); - $object = $this->factory()->withoutPersisting()->create(); + $object = static::factory()->withoutPersisting()->create(); $this->assertNull($object->id); $this->assertSame('default1', $object->getProp1()); - $this->factory()->repository()->assert()->empty(); + static::factory()->repository()->assert()->empty(); save($object); - $this->factory()->repository()->assert()->exists(['prop1' => 'default1']); + static::factory()->repository()->assert()->exists(['prop1' => 'default1']); } /** @@ -90,16 +90,16 @@ public function can_disable_auto_persist(): void */ public function can_refresh(): void { - $object = $this->factory()->create(); + $object = static::factory()->create(); // initial data $this->assertSame('default1', $object->getProp1()); - $this->factory()->repository()->assert()->exists(['prop1' => 'default1']); + static::factory()->repository()->assert()->exists(['prop1' => 'default1']); self::ensureKernelShutdown(); // modify and save title "externally" - $ext = $this->factory()->first(); + $ext = static::factory()->first(); $ext->setProp1('external'); save($ext); @@ -109,7 +109,7 @@ public function can_refresh(): void $this->assertSame($refreshed, $object); $this->assertSame('external', $object->getProp1()); - $this->factory()->repository()->assert()->exists(['prop1' => 'external']); + static::factory()->repository()->assert()->exists(['prop1' => 'external']); } /** @@ -117,18 +117,18 @@ public function can_refresh(): void */ public function cannot_refresh_if_there_are_unsaved_changes(): void { - $object = $this->factory()->create(); + $object = static::factory()->create(); // initial data $this->assertSame('default1', $object->getProp1()); - $this->factory()->repository()->assert()->exists(['prop1' => 'default1']); + static::factory()->repository()->assert()->exists(['prop1' => 'default1']); $object->setProp1('new'); try { refresh($object); } catch (\RuntimeException) { - $this->factory()->repository()->assert()->exists(['prop1' => 'default1']); + static::factory()->repository()->assert()->exists(['prop1' => 'default1']); return; } @@ -141,13 +141,13 @@ public function cannot_refresh_if_there_are_unsaved_changes(): void */ public function can_delete(): void { - $object = $this->factory()->create(); + $object = static::factory()->create(); - $this->factory()->repository()->assert()->exists(['prop1' => 'default1']); + static::factory()->repository()->assert()->exists(['prop1' => 'default1']); delete($object); - $this->factory()->repository()->assert()->empty(); + static::factory()->repository()->assert()->empty(); } /** @@ -170,9 +170,9 @@ public function repository_and_create_function(): void */ public function create_many(): void { - $models = $this->factory()->createMany(3, fn(int $i) => ['prop1' => "value{$i}"]); + $models = static::factory()->createMany(3, fn(int $i) => ['prop1' => "value{$i}"]); - $this->factory()::repository()->assert()->count(3); + static::factory()::repository()->assert()->count(3); $this->assertSame('value1', $models[0]->getProp1()); $this->assertSame('value2', $models[1]->getProp1()); @@ -184,10 +184,10 @@ public function create_many(): void */ public function find(): void { - $object = $this->factory()->create(['prop1' => 'foo']); + $object = static::factory()->create(['prop1' => 'foo']); - $this->assertSame($object->id, $this->factory()::find($object->id)->id); - $this->assertSame($object->id, $this->factory()::find(['prop1' => 'foo'])->id); + $this->assertSame($object->id, static::factory()::find($object->id)->id); + $this->assertSame($object->id, static::factory()::find(['prop1' => 'foo'])->id); } /** @@ -197,7 +197,7 @@ public function find_must_return_object(): void { $this->expectException(\RuntimeException::class); - $this->factory()::find(1); + static::factory()::find(1); } /** @@ -205,12 +205,12 @@ public function find_must_return_object(): void */ public function find_by(): void { - $this->factory()->create(['prop1' => 'a']); - $this->factory()->create(['prop1' => 'b']); - $this->factory()->create(['prop1' => 'b']); + static::factory()->create(['prop1' => 'a']); + static::factory()->create(['prop1' => 'b']); + static::factory()->create(['prop1' => 'b']); - $this->assertCount(1, $this->factory()::findBy(['prop1' => 'a'])); - $this->assertCount(2, $this->factory()::findBy(['prop1' => 'b'])); + $this->assertCount(1, static::factory()::findBy(['prop1' => 'a'])); + $this->assertCount(2, static::factory()::findBy(['prop1' => 'b'])); } /** @@ -218,15 +218,15 @@ public function find_by(): void */ public function find_or_create(): void { - $this->factory()->create(['prop1' => 'a']); + static::factory()->create(['prop1' => 'a']); - $this->assertSame('a', $this->factory()::findOrCreate(['prop1' => 'a'])->getProp1()); + $this->assertSame('a', static::factory()::findOrCreate(['prop1' => 'a'])->getProp1()); - $this->factory()::repository()->assert()->count(1); + static::factory()::repository()->assert()->count(1); - $this->assertSame('b', $this->factory()::findOrCreate(['prop1' => 'b'])->getProp1()); + $this->assertSame('b', static::factory()::findOrCreate(['prop1' => 'b'])->getProp1()); - $this->factory()::repository()->assert()->count(2); + static::factory()::repository()->assert()->count(2); } /** @@ -234,12 +234,12 @@ public function find_or_create(): void */ public function random(): void { - $this->factory()->create(['prop1' => 'a']); - $this->factory()->create(['prop1' => 'b']); - $this->factory()->create(['prop1' => 'c']); + static::factory()->create(['prop1' => 'a']); + static::factory()->create(['prop1' => 'b']); + static::factory()->create(['prop1' => 'c']); - $this->assertContains($this->factory()::random()->getProp1(), ['a', 'b', 'c']); - $this->assertSame('b', $this->factory()::random(['prop1' => 'b'])->getProp1()); + $this->assertContains(static::factory()::random()->getProp1(), ['a', 'b', 'c']); + $this->assertSame('b', static::factory()::random(['prop1' => 'b'])->getProp1()); } /** @@ -249,7 +249,7 @@ public function random_must_return_an_object(): void { $this->expectException(NotEnoughObjects::class); - $this->factory()::random(); + static::factory()::random(); } /** @@ -257,16 +257,16 @@ public function random_must_return_an_object(): void */ public function random_or_create(): void { - $this->factory()->create(['prop1' => 'a']); + static::factory()->create(['prop1' => 'a']); - $this->assertSame('a', $this->factory()::randomOrCreate()->getProp1()); - $this->assertSame('a', $this->factory()::randomOrCreate(['prop1' => 'a'])->getProp1()); + $this->assertSame('a', static::factory()::randomOrCreate()->getProp1()); + $this->assertSame('a', static::factory()::randomOrCreate(['prop1' => 'a'])->getProp1()); - $this->factory()::repository()->assert()->count(1); + static::factory()::repository()->assert()->count(1); - $this->assertSame('b', $this->factory()::randomOrCreate(['prop1' => 'b'])->getProp1()); + $this->assertSame('b', static::factory()::randomOrCreate(['prop1' => 'b'])->getProp1()); - $this->factory()::repository()->assert()->count(2); + static::factory()::repository()->assert()->count(2); } /** @@ -274,17 +274,17 @@ public function random_or_create(): void */ public function random_set(): void { - $this->factory()->create(['prop1' => 'a']); - $this->factory()->create(['prop1' => 'b']); - $this->factory()->create(['prop1' => 'b']); + static::factory()->create(['prop1' => 'a']); + static::factory()->create(['prop1' => 'b']); + static::factory()->create(['prop1' => 'b']); - $set = $this->factory()::randomSet(2); + $set = static::factory()::randomSet(2); $this->assertCount(2, $set); $this->assertContains($set[0]->getProp1(), ['a', 'b']); $this->assertContains($set[1]->getProp1(), ['a', 'b']); - $set = $this->factory()::randomSet(2, ['prop1' => 'b']); + $set = static::factory()::randomSet(2, ['prop1' => 'b']); $this->assertCount(2, $set); $this->assertSame('b', $set[0]->getProp1()); @@ -296,11 +296,11 @@ public function random_set(): void */ public function random_set_requires_at_least_the_number_available(): void { - $this->factory()::createMany(3); + static::factory()::createMany(3); $this->expectException(NotEnoughObjects::class); - $this->factory()::randomSet(4); + static::factory()::randomSet(4); } /** @@ -308,12 +308,12 @@ public function random_set_requires_at_least_the_number_available(): void */ public function random_range(): void { - $this->factory()->create(['prop1' => 'a']); - $this->factory()->create(['prop1' => 'b']); - $this->factory()->create(['prop1' => 'b']); - $this->factory()->create(['prop1' => 'b']); + static::factory()->create(['prop1' => 'a']); + static::factory()->create(['prop1' => 'b']); + static::factory()->create(['prop1' => 'b']); + static::factory()->create(['prop1' => 'b']); - $range = $this->factory()::randomRange(0, 3); + $range = static::factory()::randomRange(0, 3); $this->assertGreaterThanOrEqual(0, \count($this)); $this->assertLessThanOrEqual(3, \count($this)); @@ -322,7 +322,7 @@ public function random_range(): void $this->assertContains($object->getProp1(), ['a', 'b']); } - $range = $this->factory()::randomRange(0, 3, ['prop1' => 'b']); + $range = static::factory()::randomRange(0, 3, ['prop1' => 'b']); $this->assertGreaterThanOrEqual(0, \count($this)); $this->assertLessThanOrEqual(3, \count($this)); @@ -337,11 +337,11 @@ public function random_range(): void */ public function random_range_requires_at_least_the_max_available(): void { - $this->factory()::createMany(3); + static::factory()::createMany(3); $this->expectException(NotEnoughObjects::class); - $this->factory()::randomRange(1, 5); + static::factory()::randomRange(1, 5); } /** @@ -349,12 +349,12 @@ public function random_range_requires_at_least_the_max_available(): void */ public function factory_count(): void { - $this->factory()::createOne(['prop1' => 'a']); - $this->factory()::createOne(['prop1' => 'b']); - $this->factory()::createOne(['prop1' => 'b']); + static::factory()::createOne(['prop1' => 'a']); + static::factory()::createOne(['prop1' => 'b']); + static::factory()::createOne(['prop1' => 'b']); - $this->assertSame(3, $this->factory()::count()); - $this->assertSame(2, $this->factory()::count(['prop1' => 'b'])); + $this->assertSame(3, static::factory()::count()); + $this->assertSame(2, static::factory()::count(['prop1' => 'b'])); } /** @@ -362,12 +362,12 @@ public function factory_count(): void */ public function truncate(): void { - $this->factory()::createMany(3); - $this->factory()::repository()->assert()->count(3); + static::factory()::createMany(3); + static::factory()::repository()->assert()->count(3); - $this->factory()::truncate(); + static::factory()::truncate(); - $this->factory()::repository()->assert()->empty(); + static::factory()::repository()->assert()->empty(); } /** @@ -375,9 +375,9 @@ public function truncate(): void */ public function factory_all(): void { - $this->factory()::createMany(3); + static::factory()::createMany(3); - $this->assertCount(3, $this->factory()::all()); + $this->assertCount(3, static::factory()::all()); } /** @@ -385,14 +385,14 @@ public function factory_all(): void */ public function repository_assertions(): void { - $assert = $this->factory()::repository()->assert(); + $assert = static::factory()::repository()->assert(); $assert->empty(); $assert->empty(['prop1' => 'a']); - $this->factory()::createOne(['prop1' => 'a']); - $this->factory()::createOne(['prop1' => 'b']); - $this->factory()::createOne(['prop1' => 'b']); + static::factory()::createOne(['prop1' => 'a']); + static::factory()::createOne(['prop1' => 'b']); + static::factory()::createOne(['prop1' => 'b']); $assert->notEmpty(); $assert->notEmpty(['prop1' => 'a']); @@ -415,9 +415,9 @@ public function repository_assertions(): void */ public function repository_is_lazy(): void { - $this->factory()::createOne(); + static::factory()::createOne(); - $repository = $this->factory()::repository(); + $repository = static::factory()::repository(); $object = $repository->random(); $object->setProp1('new value'); @@ -433,21 +433,21 @@ public function repository_is_lazy(): void */ public function flush_after(): void { - $this->factory()::repository()->assert()->empty(); + static::factory()::repository()->assert()->empty(); $object = null; $return = flush_after(function() use (&$object) { - $object = $this->factory()::createOne(); + $object = static::factory()::createOne(); // ensure auto-refresh does not break when in flush_after $object->getProp1(); - $this->factory()::repository()->assert()->empty(); + static::factory()::repository()->assert()->empty(); return $object; }); - $this->factory()::repository()->assert()->count(1); + static::factory()::repository()->assert()->count(1); self::assertSame($object, $return); } @@ -456,19 +456,19 @@ public function flush_after(): void */ public function can_disable_and_enable_persisting_globally(): void { - $this->factory()::repository()->assert()->empty(); + static::factory()::repository()->assert()->empty(); disable_persisting(); - $this->factory()::createOne(); - $this->factory()::new()->create(); + static::factory()::createOne(); + static::factory()::new()->create(); persistent_factory($this->modelClass())->create(['prop1' => 'foo']); persist($this->modelClass(), ['prop1' => 'foo']); enable_persisting(); - $this->factory()::createOne(); - $this->factory()::repository()->assert()->count(1); + static::factory()::createOne(); + static::factory()::repository()->assert()->count(1); } /** @@ -480,19 +480,19 @@ public function cannot_access_repository_method_when_persist_disabled(): void $countErrors = 0; try { - $this->factory()::assert(); + static::factory()::assert(); } catch (PersistenceDisabled) { ++$countErrors; } try { - $this->factory()::repository(); + static::factory()::repository(); } catch (PersistenceDisabled) { ++$countErrors; } try { - $this->factory()::findBy([]); + static::factory()::findBy([]); } catch (PersistenceDisabled) { ++$countErrors; } @@ -505,11 +505,11 @@ public function cannot_access_repository_method_when_persist_disabled(): void */ public function can_persist_object_with_sequence(): void { - $this->factory()->sequence([['prop1' => 'foo'], ['prop1' => 'bar']])->create(); + static::factory()->sequence([['prop1' => 'foo'], ['prop1' => 'bar']])->create(); - $this->factory()::assert()->count(2); - $this->factory()::assert()->exists(['prop1' => 'foo']); - $this->factory()::assert()->exists(['prop1' => 'bar']); + static::factory()::assert()->count(2); + static::factory()::assert()->exists(['prop1' => 'foo']); + static::factory()::assert()->exists(['prop1' => 'bar']); } /** @@ -523,7 +523,7 @@ public function assert_persist_is_re_enabled_automatically(): void self::assertTrue($configuration->persistence()->isEnabled()); persist($this->modelClass(), ['prop1' => 'value']); - $this->factory()::assert()->count(1); + static::factory()::assert()->count(1); } /** @@ -531,7 +531,7 @@ public function assert_persist_is_re_enabled_automatically(): void */ public function assert_it_ca_create_object_with_dates(): void { - $object = $this->factory()->create(['date' => $date = new \DateTimeImmutable()]); + $object = static::factory()->create(['date' => $date = new \DateTimeImmutable()]); self::assertSame($date->format(\DateTimeInterface::ATOM), $object->getDate()?->format(\DateTimeInterface::ATOM)); } @@ -549,11 +549,11 @@ public function it_should_not_create_proxy_for_not_persistable_objects(): void */ protected function modelClass(): string { - return $this->factory()::class(); + return static::factory()::class(); } /** * @return PersistentObjectFactory */ - abstract protected function factory(): PersistentObjectFactory; + abstract protected static function factory(): PersistentObjectFactory; } diff --git a/tests/Integration/Persistence/GenericProxyFactoryTestCase.php b/tests/Integration/Persistence/GenericProxyFactoryTestCase.php index 6728a18c4..c8af22efb 100644 --- a/tests/Integration/Persistence/GenericProxyFactoryTestCase.php +++ b/tests/Integration/Persistence/GenericProxyFactoryTestCase.php @@ -11,6 +11,7 @@ namespace Zenstruck\Foundry\Tests\Integration\Persistence; +use PHPUnit\Framework\Attributes\Test; use Zenstruck\Foundry\Object\Instantiator; use Zenstruck\Foundry\Persistence\PersistentProxyObjectFactory; use Zenstruck\Foundry\Persistence\Proxy; @@ -18,6 +19,7 @@ use Zenstruck\Foundry\Tests\Fixture\Entity\EdgeCases\EntityWithReadonly\EntityWithReadonly; use Zenstruck\Foundry\Tests\Fixture\Model\Embeddable; use Zenstruck\Foundry\Tests\Fixture\Model\GenericModel; + use function Zenstruck\Foundry\factory; /** @@ -30,32 +32,32 @@ abstract class GenericProxyFactoryTestCase extends GenericFactoryTestCase */ public function can_update_and_delete_via_proxy(): void { - $this->factory()->repository()->assert()->empty(); + static::factory()->repository()->assert()->empty(); - $object = $this->factory()->create(); + $object = static::factory()->create(); $this->assertNotNull($object->id); $this->assertSame('default1', $object->getProp1()); $this->assertSame('default1', $object->_refresh()->getProp1()); - $this->factory()->repository()->assert() + static::factory()->repository()->assert() ->count(1) ->exists(['prop1' => 'default1']) ->notExists(['prop1' => 'invalid']) ; - $this->assertSame($object->id, $this->factory()->first()->id); - $this->assertSame($object->id, $this->factory()->last()->id); + $this->assertSame($object->id, static::factory()->first()->id); + $this->assertSame($object->id, static::factory()->last()->id); $object->setProp1('new value'); $object->_save(); $this->assertSame('new value', $object->getProp1()); - $this->factory()->repository()->assert()->exists(['prop1' => 'new value']); + static::factory()->repository()->assert()->exists(['prop1' => 'new value']); $object->_delete(); - $this->factory()->repository()->assert()->empty(); + static::factory()->repository()->assert()->empty(); } /** @@ -63,18 +65,18 @@ public function can_update_and_delete_via_proxy(): void */ public function can_disable_persisting_by_factory_and_save_proxy(): void { - $this->factory()->repository()->assert()->empty(); + static::factory()->repository()->assert()->empty(); - $object = $this->factory()->withoutPersisting()->create()->_disableAutoRefresh(); + $object = static::factory()->withoutPersisting()->create()->_disableAutoRefresh(); $this->assertNull($object->id); $this->assertSame('default1', $object->getProp1()); - $this->factory()->repository()->assert()->empty(); + static::factory()->repository()->assert()->empty(); $object->_save(); - $this->factory()->repository()->assert()->exists(['prop1' => 'default1']); + static::factory()->repository()->assert()->exists(['prop1' => 'default1']); } /** @@ -82,11 +84,11 @@ public function can_disable_persisting_by_factory_and_save_proxy(): void */ public function can_disable_and_enable_proxy_auto_refreshing(): void { - $object = $this->factory()->create(); + $object = static::factory()->create(); // initial data $this->assertSame('default1', $object->getProp1()); - $this->factory()->repository()->assert()->exists(['prop1' => 'default1']); + static::factory()->repository()->assert()->exists(['prop1' => 'default1']); $object->_disableAutoRefresh(); $object->setProp1('new'); @@ -95,7 +97,7 @@ public function can_disable_and_enable_proxy_auto_refreshing(): void $object->_save(); $this->assertSame('new 2', $object->getProp1()); - $this->factory()->repository()->assert()->exists(['prop1' => 'new 2']); + static::factory()->repository()->assert()->exists(['prop1' => 'new 2']); } /** @@ -103,11 +105,11 @@ public function can_disable_and_enable_proxy_auto_refreshing(): void */ public function can_disable_and_enable_proxy_auto_refreshing_with_callback(): void { - $object = $this->factory()->create(); + $object = static::factory()->create(); // initial data $this->assertSame('default1', $object->getProp1()); - $this->factory()->repository()->assert()->exists(['prop1' => 'default1']); + static::factory()->repository()->assert()->exists(['prop1' => 'default1']); $object->_withoutAutoRefresh(function(GenericModel&Proxy $object) { $object->setProp1('new'); @@ -116,7 +118,7 @@ public function can_disable_and_enable_proxy_auto_refreshing_with_callback(): vo }); $this->assertSame('new 2', $object->getProp1()); - $this->factory()->repository()->assert()->exists(['prop1' => 'new 2']); + static::factory()->repository()->assert()->exists(['prop1' => 'new 2']); } /** @@ -124,16 +126,16 @@ public function can_disable_and_enable_proxy_auto_refreshing_with_callback(): vo */ public function can_manually_refresh_via_proxy(): void { - $object = $this->factory()->create()->_disableAutoRefresh(); + $object = static::factory()->create()->_disableAutoRefresh(); // initial data $this->assertSame('default1', $object->getProp1()); - $this->factory()->repository()->assert()->exists(['prop1' => 'default1']); + static::factory()->repository()->assert()->exists(['prop1' => 'default1']); self::ensureKernelShutdown(); // modify and save title "externally" - $ext = $this->factory()::first(); + $ext = static::factory()::first(); $ext->setProp1('external'); $ext->_save(); @@ -143,7 +145,7 @@ public function can_manually_refresh_via_proxy(): void // "calling method" triggers auto-refresh $this->assertSame('external', $object->getProp1()); - $this->factory()->repository()->assert()->exists(['prop1' => 'external']); + static::factory()->repository()->assert()->exists(['prop1' => 'external']); } /** @@ -151,16 +153,16 @@ public function can_manually_refresh_via_proxy(): void */ public function proxy_auto_refreshes(): void { - $object = $this->factory()->create(); + $object = static::factory()->create(); // initial data $this->assertSame('default1', $object->getProp1()); - $this->factory()->repository()->assert()->exists(['prop1' => 'default1']); + static::factory()->repository()->assert()->exists(['prop1' => 'default1']); self::ensureKernelShutdown(); // modify and save title "externally" - $ext = $this->factory()::first(); + $ext = static::factory()::first(); $ext->setProp1('external'); $ext->_save(); @@ -168,7 +170,7 @@ public function proxy_auto_refreshes(): void // "calling method" triggers auto-refresh $this->assertSame('external', $object->getProp1()); - $this->factory()->repository()->assert()->exists(['prop1' => 'external']); + static::factory()->repository()->assert()->exists(['prop1' => 'external']); } /** @@ -176,21 +178,21 @@ public function proxy_auto_refreshes(): void */ public function cannot_auto_refresh_proxy_if_changes(): void { - $object = $this->factory()->create(); + $object = static::factory()->create(); // initial data $this->assertSame('default1', $object->getProp1()); - $this->factory()->repository()->assert()->exists(['prop1' => 'default1']); + static::factory()->repository()->assert()->exists(['prop1' => 'default1']); $object->setProp1('new'); try { $object->setProp1('new 1'); } catch (\RuntimeException) { - $this->factory()->repository()->assert()->exists(['prop1' => 'default1']); + static::factory()->repository()->assert()->exists(['prop1' => 'default1']); $object->_save(); $this->assertSame('new', $object->getProp1()); - $this->factory()->repository()->assert()->exists(['prop1' => 'new']); + static::factory()->repository()->assert()->exists(['prop1' => 'new']); return; } @@ -203,11 +205,11 @@ public function cannot_auto_refresh_proxy_if_changes(): void */ public function can_access_repository_from_proxy(): void { - $object = $this->factory()::createOne(); + $object = static::factory()::createOne(); $object = $object->_repository()->findOneBy(['prop1' => 'default1']); - $this->assertInstanceOf($this->factory()::class(), $object); + $this->assertInstanceOf(static::factory()::class(), $object); } /** @@ -215,7 +217,7 @@ public function can_access_repository_from_proxy(): void */ public function can_force_set_and_get_proxy(): void { - $object = $this->factory()::createOne(); + $object = static::factory()::createOne(); $this->assertSame('default1', $object->_get('prop1')); @@ -229,7 +231,7 @@ public function can_force_set_and_get_proxy(): void */ public function can_get_real_object_even_if_modified(): void { - $object = $this->factory()->create(); + $object = static::factory()->create(); $object->setProp1('foo'); self::assertInstanceOf(GenericModel::class, $real = $object->_real()); @@ -246,7 +248,7 @@ public function can_create_object_with_readonly_properties(): void $objectWithReadOnly = $factory::createOne([ 'prop' => 1, 'embedded' => factory(Embeddable::class, ['prop1' => 'value1']), - 'date' => new \DateTimeImmutable() + 'date' => new \DateTimeImmutable(), ]); $objectWithReadOnly->_refresh(); @@ -259,7 +261,7 @@ public function can_create_object_with_readonly_properties(): void */ public function can_delete_proxified_object_and_still_access_its_methods(): void { - $object = $this->factory()->create(); + $object = static::factory()->create(); $object->_delete(); $this->assertSame('default1', $object->getProp1()); @@ -270,9 +272,9 @@ public function can_delete_proxified_object_and_still_access_its_methods(): void */ public function can_use_after_persist_with_attributes(): void { - $object = $this->factory() + $object = static::factory() ->instantiateWith(Instantiator::withConstructor()->allowExtra('extra')) - ->afterPersist(function (GenericModel $object, array $attributes) { + ->afterPersist(function(GenericModel $object, array $attributes) { $object->setProp1($attributes['extra']); }) ->create(['extra' => $value = 'value set with after persist']); @@ -283,7 +285,7 @@ public function can_use_after_persist_with_attributes(): void /** * @return PersistentProxyObjectFactory */ - abstract protected function factory(): PersistentProxyObjectFactory; // @phpstan-ignore method.childReturnType + abstract protected static function factory(): PersistentProxyObjectFactory; /** * @return PersistentProxyObjectFactory From 3a3c7d678530f3885e51c4d081d028efad5ed875 Mon Sep 17 00:00:00 2001 From: nikophil Date: Thu, 24 Oct 2024 09:58:29 +0000 Subject: [PATCH 004/102] bot: fix cs [skip ci] --- .../BootFoundryOnDataProviderMethodCalled.php | 4 +- src/PHPUnit/FoundryExtension.php | 4 +- ...ownFoundryOnDataProviderMethodFinished.php | 4 +- src/Test/Factories.php | 76 +++++++++---------- 4 files changed, 43 insertions(+), 45 deletions(-) diff --git a/src/PHPUnit/BootFoundryOnDataProviderMethodCalled.php b/src/PHPUnit/BootFoundryOnDataProviderMethodCalled.php index d9035fc91..2eccf412c 100644 --- a/src/PHPUnit/BootFoundryOnDataProviderMethodCalled.php +++ b/src/PHPUnit/BootFoundryOnDataProviderMethodCalled.php @@ -23,8 +23,8 @@ final class BootFoundryOnDataProviderMethodCalled implements Event\Test\DataProv { public function notify(Event\Test\DataProviderMethodCalled $event): void { - if (method_exists($event->testMethod()->className(), '_bootForDataProvider')) { - call_user_func([$event->testMethod()->className(), '_bootForDataProvider']); + if (\method_exists($event->testMethod()->className(), '_bootForDataProvider')) { + \call_user_func([$event->testMethod()->className(), '_bootForDataProvider']); } } } diff --git a/src/PHPUnit/FoundryExtension.php b/src/PHPUnit/FoundryExtension.php index c6ac7ddd0..738a1a16c 100644 --- a/src/PHPUnit/FoundryExtension.php +++ b/src/PHPUnit/FoundryExtension.php @@ -32,9 +32,7 @@ public function bootstrap( Runner\Extension\ParameterCollection $parameters, ): void { if (!ConstraintRequirement::from(self::MIN_PHPUNIT_VERSION)->isSatisfiedBy(Runner\Version::id())) { - throw new \LogicException( - \sprintf('Your PHPUnit version (%s) is not compatible with the minimum version (%s) needed to use this extension.', Runner\Version::id(), self::MIN_PHPUNIT_VERSION) - ); + throw new \LogicException(\sprintf('Your PHPUnit version (%s) is not compatible with the minimum version (%s) needed to use this extension.', Runner\Version::id(), self::MIN_PHPUNIT_VERSION)); } // shutdown Foundry if for some reason it has been booted before diff --git a/src/PHPUnit/ShutdownFoundryOnDataProviderMethodFinished.php b/src/PHPUnit/ShutdownFoundryOnDataProviderMethodFinished.php index 57e34daa8..6fbe1394b 100644 --- a/src/PHPUnit/ShutdownFoundryOnDataProviderMethodFinished.php +++ b/src/PHPUnit/ShutdownFoundryOnDataProviderMethodFinished.php @@ -23,8 +23,8 @@ final class ShutdownFoundryOnDataProviderMethodFinished implements Event\Test\Da { public function notify(Event\Test\DataProviderMethodFinished $event): void { - if (method_exists($event->testMethod()->className(), '_shutdownAfterDataProvider')) { - call_user_func([$event->testMethod()->className(), '_shutdownAfterDataProvider']); + if (\method_exists($event->testMethod()->className(), '_shutdownAfterDataProvider')) { + \call_user_func([$event->testMethod()->className(), '_shutdownAfterDataProvider']); } } } diff --git a/src/Test/Factories.php b/src/Test/Factories.php index 4f31432a6..1f31d3abb 100644 --- a/src/Test/Factories.php +++ b/src/Test/Factories.php @@ -44,6 +44,44 @@ public static function _shutdownFoundry(): void Configuration::shutdown(); } + /** + * @see \Zenstruck\Foundry\PHPUnit\BootFoundryOnDataProviderMethodCalled + * @internal + */ + public static function _bootForDataProvider(): void + { + if (!\is_subclass_of(static::class, KernelTestCase::class)) { // @phpstan-ignore function.impossibleType + // unit test + Configuration::bootForDataProvider(UnitTestConfig::build()); + + return; + } + + // integration test + Configuration::bootForDataProvider(static function() { + 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 + }); + } + + /** + * @internal + * @see \Zenstruck\Foundry\PHPUnit\ShutdownFoundryOnDataProviderMethodFinished + */ + public static function _shutdownAfterDataProvider(): void + { + if (\is_subclass_of(static::class, KernelTestCase::class)) { // @phpstan-ignore function.impossibleType + self::ensureKernelShutdown(); // @phpstan-ignore staticMethod.notFound + static::$class = null; // @phpstan-ignore staticProperty.notFound + static::$kernel = null; // @phpstan-ignore staticProperty.notFound + static::$booted = false; // @phpstan-ignore staticProperty.notFound + } + Configuration::shutdown(); + } + /** * @internal */ @@ -98,42 +136,4 @@ private function _loadDataProvidedProxies(): void initialize_proxy_object($providedData); } - - /** - * @see \Zenstruck\Foundry\PHPUnit\BootFoundryOnDataProviderMethodCalled - * @internal - */ - public static function _bootForDataProvider(): void - { - if (!\is_subclass_of(static::class, KernelTestCase::class)) { // @phpstan-ignore function.impossibleType - // unit test - Configuration::bootForDataProvider(UnitTestConfig::build()); - - return; - } - - // integration test - Configuration::bootForDataProvider(static function() { - 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 - }); - } - - /** - * @internal - * @see \Zenstruck\Foundry\PHPUnit\ShutdownFoundryOnDataProviderMethodFinished - */ - public static function _shutdownAfterDataProvider(): void - { - if (\is_subclass_of(static::class, KernelTestCase::class)) { // @phpstan-ignore function.impossibleType - self::ensureKernelShutdown(); // @phpstan-ignore staticMethod.notFound - static::$class = null; // @phpstan-ignore staticProperty.notFound - static::$kernel = null; // @phpstan-ignore staticProperty.notFound - static::$booted = false; // @phpstan-ignore staticProperty.notFound - } - Configuration::shutdown(); - } } From 470d927a4121067685c2c54cc60334c854b4c627 Mon Sep 17 00:00:00 2001 From: Nicolas PHILIPPE Date: Thu, 24 Oct 2024 14:13:15 +0200 Subject: [PATCH 005/102] docs: how to extend database reset mechanism (#706) --- docs/index.rst | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/docs/index.rst b/docs/index.rst index 88a216778..2a2d50c13 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1321,6 +1321,45 @@ files. - '%kernel.root_dir%/migrations/configuration.php' - 'migrations/configuration.yaml' +Extending reset mechanism +......................... + +The reset mechanism can be extended thanks to decoration: + +:: + + use Symfony\Component\DependencyInjection\Attribute\AsDecorator; + use Symfony\Component\DependencyInjection\Attribute\When; + use Symfony\Component\HttpKernel\KernelInterface; + use Zenstruck\Foundry\ORM\ResetDatabase\OrmResetter; + + // The decorator should be declared in test environment only. + #[When('test')] + // You can also decorate `MongoResetter::class`. + #[AsDecorator(OrmResetter::class)] + final readonly class DecorateDatabaseResetter implements OrmResetter + { + public function __construct( + private OrmResetter $decorated + ) {} + + public function resetBeforeFirstTest(KernelInterface $kernel): void + { + // do something once per test suite (for instance: install a PostgreSQL extension) + + $this->decorated->resetBeforeFirstTest($kernel); + } + + public function resetBeforeEachTest(KernelInterface $kernel): void + { + // do something once per test case (for instance: restart PostgreSQL sequences) + + $this->decorated->resetBeforeEachTest($kernel); + } + } + +If using a standard Symfony Flex app, this will be autowired/autoconfigured. If not, register the service + .. _object-proxy: Object Proxy From a549c10c27b83eee52807b6be4f56cb578b51566 Mon Sep 17 00:00:00 2001 From: Nicolas PHILIPPE Date: Thu, 24 Oct 2024 14:17:15 +0200 Subject: [PATCH 006/102] docs: using factories in data providers (#707) --- docs/index.rst | 63 ++++++++++++++++++- ...rForServiceFactoryInKernelTestCaseTest.php | 12 ++-- .../DataProvider/DataProviderInUnitTest.php | 14 +++++ 3 files changed, 82 insertions(+), 7 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index 2a2d50c13..751e4b0d1 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1607,7 +1607,68 @@ PHPUnit Data Providers ~~~~~~~~~~~~~~~~~~~~~~ It is possible to use factories in -`PHPUnit data providers `_: +`PHPUnit data providers `_. +Their usage depends on which Foundry version you are running: + +Data Providers with Foundry ^2.2 +................................ + +From version 2.2, Foundry provides an extension for PHPUnit. +You can install it by modifying you ``phpunit.xml.dist``: + +.. configuration-block:: + + .. code-block:: xml + + + + + + + +.. warning:: + + This PHPUnit extension requires at least PHPUnit 11.4. + +Using this extension will allow to use your factories in your data providers the same way you're using them in tests. +Thanks to it, you can: + * Call ``->create()`` or ``::createOne()`` or any other method which creates objects in unit tests + (using ``PHPUnit\Framework\TestCase``) and functional tests (``Symfony\Bundle\FrameworkBundle\Test\KernelTestCase``) + * Use `Factories as Services`_ in functional tests + * Use `faker()` normally, without wrapping its call in a callable + +:: + + use App\Factory\PostFactory; + use PHPUnit\Framework\Attributes\DataProvider; + + #[DataProvider('createMultipleObjectsInDataProvider')] + public function test_post_via_data_provider(Post $post): void + { + // at this point, `$post` exists, and is already stored in database + } + + public static function postDataProvider(): iterable + { + yield [PostFactory::createOne()]; + yield [PostWithServiceFactory::createOne()]; + yield [PostFactory::createOne(['body' => faker()->sentence()]; + } + + +.. warning:: + + Because Foundry is relying on its `Proxy mechanism `_, when using persistence, + your factories must extend ``Zenstruck\Foundry\Persistence\PersistentProxyObjectFactory`` to work in your data providers. + +.. warning:: + + For the same reason, you should not call methods from `Proxy` class in your data providers, + not even ``->_real()``. + + +Data Providers before Foundry v2.2 +.................................. :: diff --git a/tests/Integration/DataProvider/DataProviderForServiceFactoryInKernelTestCaseTest.php b/tests/Integration/DataProvider/DataProviderForServiceFactoryInKernelTestCaseTest.php index 4d16399c9..5883b1c02 100644 --- a/tests/Integration/DataProvider/DataProviderForServiceFactoryInKernelTestCaseTest.php +++ b/tests/Integration/DataProvider/DataProviderForServiceFactoryInKernelTestCaseTest.php @@ -24,6 +24,8 @@ use Zenstruck\Foundry\Tests\Fixture\Factories\Object1Factory; use Zenstruck\Foundry\Tests\Fixture\Object1; +use function Zenstruck\Foundry\faker; + /** * @author Nicolas PHILIPPE * @requires PHPUnit 11.4 @@ -34,23 +36,21 @@ final class DataProviderForServiceFactoryInKernelTestCaseTest extends KernelTest { use Factories; - /** - * @test - */ #[Test] #[DataProvider('createObjectFromServiceFactoryInDataProvider')] - public function it_can_create_one_object_in_data_provider(?Object1 $providedData): void + public function it_can_create_one_object_in_data_provider(?Object1 $providedData, string $expected): void { self::assertFalse(Configuration::instance()->inADataProvider()); self::assertInstanceOf(Object1::class, $providedData); - $this->assertSame('router-constructor', $providedData->getProp1()); + $this->assertSame($expected, $providedData->getProp1()); } public static function createObjectFromServiceFactoryInDataProvider(): iterable { yield 'service factory' => [ - Object1Factory::createOne(), + Object1Factory::createOne(['prop1' => $prop1 = faker()->sentence()]), + "$prop1-constructor" ]; } } diff --git a/tests/Integration/DataProvider/DataProviderInUnitTest.php b/tests/Integration/DataProvider/DataProviderInUnitTest.php index 6eda443a7..ddf83fb91 100644 --- a/tests/Integration/DataProvider/DataProviderInUnitTest.php +++ b/tests/Integration/DataProvider/DataProviderInUnitTest.php @@ -28,6 +28,7 @@ use Zenstruck\Foundry\Tests\Fixture\Object1; use Zenstruck\Foundry\Tests\Fixture\Object2; +use function Zenstruck\Foundry\faker; use function Zenstruck\Foundry\Persistence\unproxy; /** @@ -65,4 +66,17 @@ public static function createObjectWithPersistentObjectFactoryInDataProvider(): yield 'persistent factory' => [GenericEntityFactory::createOne(), new GenericEntity('default1')]; yield 'proxy persistent factory' => [GenericProxyEntityFactory::createOne(), new GenericEntity('default1')]; } + + #[Test] + #[DataProvider('createObjectUsingFakerInDataProvider')] + public function assert_it_can_create_use_faker_in_data_provider(mixed $providedData, string $expected): void + { + self::assertSame($expected, $providedData->getProp1()); + } + + public static function createObjectUsingFakerInDataProvider(): iterable + { + yield 'object factory' => [Object1Factory::createOne(['prop1' => $prop1 = faker()->sentence()]), "$prop1-constructor"]; + yield 'persistent factory' => [GenericEntityFactory::createOne(['prop1' => $prop1 = faker()->sentence()]), $prop1]; + } } From a04170e538aa63789ce89f25743b1be60879e296 Mon Sep 17 00:00:00 2001 From: Kevin Bond Date: Thu, 24 Oct 2024 15:15:33 -0400 Subject: [PATCH 007/102] changelog: update [skip ci] --- CHANGELOG.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 06debdd25..597469122 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # CHANGELOG +## [v2.2.0](https://github.com/zenstruck/foundry/releases/tag/v2.2.0) + +October 24th, 2024 - [v2.1.0...v2.2.0](https://github.com/zenstruck/foundry/compare/v2.1.0...v2.2.0) + +* a549c10 docs: using factories in data providers (#707) by @nikophil +* 470d927 docs: how to extend database reset mechanism (#706) by @nikophil +* 2014ed9 feature: allow to use `Factory::create()` and factory service in data providers (#648) by @nikophil +* df568da refactor: make "database reset" mechanism extendable (#690) by @nikophil +* 4fb0b25 docs: add missing docs (#703) by @nikophil +* fa1d527 minor: misc fixes for sca (#705) by @nikophil +* 0d570cc refactor: fix proxy system and introduce psalm extension (#704) by @nikophil + ## [v2.1.0](https://github.com/zenstruck/foundry/releases/tag/v2.1.0) October 3rd, 2024 - [v2.0.9...v2.1.0](https://github.com/zenstruck/foundry/compare/v2.0.9...v2.1.0) From 2b12ef092f0ee3f0e65e00d54fa11fc5977fe212 Mon Sep 17 00:00:00 2001 From: Nicolas PHILIPPE Date: Fri, 25 Oct 2024 12:45:21 +0200 Subject: [PATCH 008/102] chore: simplify CI matrix (#708) --- .github/workflows/ci.yml | 131 ++++++++------------------------------- phpunit | 19 +++--- 2 files changed, 38 insertions(+), 112 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a29b02801..8ae277c4f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,119 +8,44 @@ on: jobs: tests: - name: P:${{ matrix.php }}, S:${{ matrix.symfony }}, D:${{ matrix.database }}, PU:${{ matrix.phpunit }}${{ matrix.deps == 'lowest' && ' (lowest)' || '' }}${{ matrix.use-dama == 1 && contains(matrix.database, 'sql') && ' (dama)' || '' }}${{ !contains(matrix.database, 'sql') && '' || matrix.use-migrate == 1 && ' (migrate)' || ' (schema)' }}${{ matrix.use-phpunit-extension == 1 && ' (phpunit extension)' || '' }} + 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)' || '' }}${{ !contains(matrix.database, 'sql') && '' || matrix.use-migrate == 1 && ' (migrate)' || ' (schema)' }}${{ matrix.use-phpunit-extension == 1 && ' (phpunit extension)' || '' }} runs-on: ubuntu-latest strategy: fail-fast: false matrix: php: [ 8.1, 8.2, 8.3 ] - deps: [ highest ] - symfony: [ 6.4.*, 7.0.*, 7.1.* ] + symfony: [ 6.4.*, 7.1.* ] database: [ mysql, mongo ] - use-dama: [ 1 ] - use-migrate: [ 0 ] - use-phpunit-extension: [ 0 ] - phpunit: [ 9 ] + + # default values: + # deps: [ highest ] + # without-dama: [ 0 ] + # use-migrate: [ 0 ] + # use-phpunit-extension: [ 0 ] + # phpunit: [ 9 ] + exclude: - - php: 8.1 - symfony: 7.0.* - - php: 8.1 - symfony: 7.1.* + - {php: 8.1, symfony: 7.1.*} include: - - php: 8.3 - deps: highest - symfony: '*' - database: none - use-dama: 1 - use-migrate: 0 - use-phpunit-extension: 0 - phpunit: 9 - - php: 8.3 - deps: highest - symfony: '*' - database: mysql|mongo - use-dama: 1 - use-migrate: 0 - use-phpunit-extension: 0 - phpunit: 9 - - php: 8.3 - deps: highest - symfony: '*' - database: pgsql|mongo - use-dama: 1 - use-migrate: 0 - use-phpunit-extension: 0 - phpunit: 9 - - php: 8.3 - deps: highest - symfony: '*' - database: pgsql - use-dama: 0 - use-migrate: 0 - use-phpunit-extension: 0 - phpunit: 9 - - php: 8.3 - deps: highest - symfony: '*' - database: sqlite - use-dama: 0 - use-migrate: 0 - use-phpunit-extension: 0 - phpunit: 9 - - php: 8.3 - deps: lowest - symfony: '*' - database: sqlite - use-dama: 0 - use-migrate: 0 - use-phpunit-extension: 0 - phpunit: 9 - - php: 8.3 - deps: lowest - symfony: '*' - database: mysql - use-dama: 1 - use-migrate: 0 - use-phpunit-extension: 0 - phpunit: 9 - - php: 8.3 - deps: highest - symfony: '*' - database: mysql - use-dama: 1 - use-migrate: 1 - use-phpunit-extension: 0 - phpunit: 9 - - php: 8.3 - deps: highest - symfony: '*' - database: mysql|mongo - use-dama: 1 - use-migrate: 0 - use-phpunit-extension: 0 - phpunit: 10 - - php: 8.3 - deps: highest - symfony: '*' - database: mysql|mongo - use-dama: 1 - use-migrate: 0 - use-phpunit-extension: 0 - phpunit: 11 - - php: 8.3 - deps: highest - symfony: '*' - database: mysql|mongo - use-dama: 1 - use-migrate: 0 - use-phpunit-extension: 1 - phpunit: 11 + - {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, use-migrate: 1} + - {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} env: DATABASE_URL: ${{ contains(matrix.database, 'mysql') && 'mysql://root:root@localhost:3306/foundry?serverVersion=5.7.42' || contains(matrix.database, 'pgsql') && 'postgresql://root:root@localhost:5432/foundry?serverVersion=15' || contains(matrix.database, 'sqlite') && 'sqlite:///%kernel.project_dir%/var/data.db' || '' }} MONGO_URL: ${{ contains(matrix.database, 'mongo') && 'mongodb://127.0.0.1:27017/dbName?compressors=disabled&gssapiServiceName=mongodb' || '' }} - USE_DAMA_DOCTRINE_TEST_BUNDLE: ${{ matrix.use-dama == 1 && contains(matrix.database, 'sql') && 1 || 0 }} - USE_FOUNDRY_PHPUNIT_EXTENSION: ${{ matrix.use-phpunit-extension }} - PHPUNIT_VERSION: ${{ matrix.phpunit }} + USE_DAMA_DOCTRINE_TEST_BUNDLE: ${{ matrix.without-dama == 0 && contains(matrix.database, 'sql') && 1 || 0 }} + USE_FOUNDRY_PHPUNIT_EXTENSION: ${{ matrix.use-phpunit-extension || 0 }} + PHPUNIT_VERSION: ${{ matrix.phpunit || 9 }} + DATABASE_RESET_MODE: ${{ matrix.use-migrate == 1 && 'migrate' || 'schema' }} services: postgres: image: ${{ contains(matrix.database, 'pgsql') && 'postgres:15' || '' }} @@ -165,8 +90,6 @@ jobs: - name: Test run: ./phpunit shell: bash - env: - DATABASE_RESET_MODE: ${{ matrix.use-migrate == 1 && 'migrate' || 'schema' }} code-coverage: name: Code Coverage diff --git a/phpunit b/phpunit index 60c41819a..c291e79ae 100755 --- a/phpunit +++ b/phpunit @@ -48,22 +48,23 @@ if [ "${USE_FOUNDRY_PHPUNIT_EXTENSION:-0}" = "1" ] && [ "${PHPUNIT_VERSION}" != exit 1; fi +PHPUNIT_EXEC="vendor/bin/phpunit" case ${PHPUNIT_VERSION} in "9") + PHPUNIT_EXEC="${PHPUNIT_EXEC} -c phpunit.xml.dist" if [ "${USE_DAMA_DOCTRINE_TEST_BUNDLE:-0}" = "1" ]; then - vendor/bin/phpunit -c phpunit.xml.dist --extensions "${DAMA_EXTENSION}" "$@" - else - vendor/bin/phpunit -c phpunit.xml.dist "$@" + PHPUNIT_EXEC="${PHPUNIT_EXEC} --extensions ${DAMA_EXTENSION}" fi ;; "10") # PHPUnit 10 does not have a --extension option - vendor/bin/phpunit -c phpunit-10.xml.dist "$@" + PHPUNIT_EXEC="${PHPUNIT_EXEC} -c phpunit-10.xml.dist" ;; "11") - PHPUNIT_EXEC="vendor/bin/phpunit -c phpunit-10.xml.dist $@" + PHPUNIT_EXEC="${PHPUNIT_EXEC} -c phpunit-10.xml.dist" + if [ "${USE_DAMA_DOCTRINE_TEST_BUNDLE:-0}" = "1" ]; then PHPUNIT_EXEC="${PHPUNIT_EXEC} --extension "${DAMA_EXTENSION}"" fi @@ -71,9 +72,11 @@ case ${PHPUNIT_VERSION} in if [ "${USE_FOUNDRY_PHPUNIT_EXTENSION:-0}" = "1" ]; then PHPUNIT_EXEC="${PHPUNIT_EXEC} --extension "${FOUNDRY_EXTENSION}"" fi - - echo $PHPUNIT_EXEC - $PHPUNIT_EXEC ;; esac + +PHPUNIT_EXEC="${PHPUNIT_EXEC} ${@}" + +echo "${PHPUNIT_EXEC}" +$PHPUNIT_EXEC ### << From 6d772b7291740efaeb09a025903c481d8ebefa1b Mon Sep 17 00:00:00 2001 From: nikophil Date: Fri, 25 Oct 2024 10:46:37 +0000 Subject: [PATCH 009/102] bot: fix cs [skip ci] --- .../DataProviderForServiceFactoryInKernelTestCaseTest.php | 2 +- tests/Integration/DataProvider/DataProviderInUnitTest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Integration/DataProvider/DataProviderForServiceFactoryInKernelTestCaseTest.php b/tests/Integration/DataProvider/DataProviderForServiceFactoryInKernelTestCaseTest.php index 5883b1c02..6a1df8e34 100644 --- a/tests/Integration/DataProvider/DataProviderForServiceFactoryInKernelTestCaseTest.php +++ b/tests/Integration/DataProvider/DataProviderForServiceFactoryInKernelTestCaseTest.php @@ -50,7 +50,7 @@ public static function createObjectFromServiceFactoryInDataProvider(): iterable { yield 'service factory' => [ Object1Factory::createOne(['prop1' => $prop1 = faker()->sentence()]), - "$prop1-constructor" + "{$prop1}-constructor", ]; } } diff --git a/tests/Integration/DataProvider/DataProviderInUnitTest.php b/tests/Integration/DataProvider/DataProviderInUnitTest.php index ddf83fb91..6b0089c12 100644 --- a/tests/Integration/DataProvider/DataProviderInUnitTest.php +++ b/tests/Integration/DataProvider/DataProviderInUnitTest.php @@ -76,7 +76,7 @@ public function assert_it_can_create_use_faker_in_data_provider(mixed $providedD public static function createObjectUsingFakerInDataProvider(): iterable { - yield 'object factory' => [Object1Factory::createOne(['prop1' => $prop1 = faker()->sentence()]), "$prop1-constructor"]; + yield 'object factory' => [Object1Factory::createOne(['prop1' => $prop1 = faker()->sentence()]), "{$prop1}-constructor"]; yield 'persistent factory' => [GenericEntityFactory::createOne(['prop1' => $prop1 = faker()->sentence()]), $prop1]; } } From dfeb247a15d803c4655bd8a38b6034abd8422428 Mon Sep 17 00:00:00 2001 From: Nicolas PHILIPPE Date: Fri, 25 Oct 2024 13:16:17 +0200 Subject: [PATCH 010/102] chore: test Foundry on PHP 8.4 & sf 7.2 (#709) --- .github/workflows/ci.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8ae277c4f..f994b6213 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,8 +13,8 @@ jobs: strategy: fail-fast: false matrix: - php: [ 8.1, 8.2, 8.3 ] - symfony: [ 6.4.*, 7.1.* ] + php: [ 8.1, 8.2, 8.3, 8.4 ] + symfony: [ 6.4.*, 7.1.*, 7.2.* ] database: [ mysql, mongo ] # default values: @@ -26,6 +26,7 @@ jobs: 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} From 496a7a831f674d7ab9d64bf4c34972a142800c83 Mon Sep 17 00:00:00 2001 From: berumuron Date: Thu, 31 Oct 2024 13:04:28 +0100 Subject: [PATCH 011/102] fix: Change `RepositoryDecorator::inner()` visibility to public (#714) --- src/Persistence/RepositoryDecorator.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Persistence/RepositoryDecorator.php b/src/Persistence/RepositoryDecorator.php index a64ca53bf..3d5422c5b 100644 --- a/src/Persistence/RepositoryDecorator.php +++ b/src/Persistence/RepositoryDecorator.php @@ -229,7 +229,7 @@ public function getIterator(): \Traversable /** * @return ObjectRepository */ - private function inner(): ObjectRepository + public function inner(): ObjectRepository { return Configuration::instance()->persistence()->repositoryFor($this->class); } From 4743a9da142b42d484ac7a99c1a7068aa66c7bec Mon Sep 17 00:00:00 2001 From: Nicolas PHILIPPE Date: Thu, 31 Oct 2024 13:10:58 +0100 Subject: [PATCH 012/102] changelog: update [skip ci] --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 597469122..d2b4c3a59 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # CHANGELOG +## [v2.2.1](https://github.com/zenstruck/foundry/releases/tag/v2.2.1) + +October 31st, 2024 - [v2.2.0...v2.2.1](https://github.com/zenstruck/foundry/compare/v2.2.0...v2.2.1) + +* 496a7a8 fix: Change `RepositoryDecorator::inner()` visibility to public (#714) by @marienfressinaud +* dfeb247 chore: test Foundry on PHP 8.4 & sf 7.2 (#709) by @nikophil +* 2b12ef0 chore: simplify CI matrix (#708) by @nikophil + ## [v2.2.0](https://github.com/zenstruck/foundry/releases/tag/v2.2.0) October 24th, 2024 - [v2.1.0...v2.2.0](https://github.com/zenstruck/foundry/compare/v2.1.0...v2.2.0) From 870cb4209b736481ced65f024212bd052db7cb12 Mon Sep 17 00:00:00 2001 From: Dimitri Dovgan <74016280+justpilot@users.noreply.github.com> Date: Tue, 5 Nov 2024 13:35:54 +0100 Subject: [PATCH 013/102] docs: fix missing comma in upgrade doc (#718) --- UPGRADE-2.0.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/UPGRADE-2.0.md b/UPGRADE-2.0.md index 9eb13b5e1..a3d5801cd 100644 --- a/UPGRADE-2.0.md +++ b/UPGRADE-2.0.md @@ -53,7 +53,7 @@ use Zenstruck\Foundry\Utils\Rector\FoundrySetList; return RectorConfig::configure() ->withPaths([ // add all paths where your factories are defined and where Foundry is used - 'src/Factory' + 'src/Factory', 'tests' ]) ->withSets([FoundrySetList::UP_TO_FOUNDRY_2]) @@ -92,6 +92,8 @@ If the mapping is defined outside the code, with xml, yaml or php configuration, 1. Create a `tests/object-manager.php` file which will expose your doctrine config. Here is an example: ```php +object()` (or, now, `_real()`) everywhere to satisf - `enableAutoRefresh()` -> `_enableAutoRefresh()` - `disableAutoRefresh()` -> `_disableAutoRefresh()` - `withoutAutoRefresh()` -> `_withoutAutoRefresh()` - - `assertPersisted()` -> `_assertPersisted()` + - `assertPersisted()` -> `_assertPersisted()` - `assertNotPersisted()` -> `_assertNotPersisted()` - `isPersisted()` is removed without any replacement - `forceSetAll()` is removed without any replacement From 3282f248c946e029b2e04961e9780f25b6f86c02 Mon Sep 17 00:00:00 2001 From: HypeMC <2445045+HypeMC@users.noreply.github.com> Date: Tue, 5 Nov 2024 17:33:03 +0100 Subject: [PATCH 014/102] Remove @internal from db resetter interfaces (#715) --- src/Persistence/ResetDatabase/BeforeEachTestResetter.php | 1 - src/Persistence/ResetDatabase/BeforeFirstTestResetter.php | 1 - 2 files changed, 2 deletions(-) diff --git a/src/Persistence/ResetDatabase/BeforeEachTestResetter.php b/src/Persistence/ResetDatabase/BeforeEachTestResetter.php index f8ad54bdb..314e16a8c 100644 --- a/src/Persistence/ResetDatabase/BeforeEachTestResetter.php +++ b/src/Persistence/ResetDatabase/BeforeEachTestResetter.php @@ -16,7 +16,6 @@ use Symfony\Component\HttpKernel\KernelInterface; /** - * @internal * @author Nicolas PHILIPPE */ interface BeforeEachTestResetter diff --git a/src/Persistence/ResetDatabase/BeforeFirstTestResetter.php b/src/Persistence/ResetDatabase/BeforeFirstTestResetter.php index 257b15d13..14ef2041c 100644 --- a/src/Persistence/ResetDatabase/BeforeFirstTestResetter.php +++ b/src/Persistence/ResetDatabase/BeforeFirstTestResetter.php @@ -16,7 +16,6 @@ use Symfony\Component\HttpKernel\KernelInterface; /** - * @internal * @author Nicolas PHILIPPE */ interface BeforeFirstTestResetter From 44eec45b620fd5419906c387512bd1acdf898647 Mon Sep 17 00:00:00 2001 From: Nicolas PHILIPPE Date: Tue, 5 Nov 2024 17:39:17 +0100 Subject: [PATCH 015/102] changelog: update [skip ci] --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d2b4c3a59..6ec839323 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # CHANGELOG +## [v2.2.2](https://github.com/zenstruck/foundry/releases/tag/v2.2.2) + +November 5th, 2024 - [v2.2.1...v2.2.2](https://github.com/zenstruck/foundry/compare/v2.2.1...v2.2.2) + +* 3282f24 Remove @internal from db resetter interfaces (#715) by @HypeMC +* 870cb42 docs: fix missing comma in upgrade doc (#718) by @justpilot + ## [v2.2.1](https://github.com/zenstruck/foundry/releases/tag/v2.2.1) October 31st, 2024 - [v2.2.0...v2.2.1](https://github.com/zenstruck/foundry/compare/v2.2.0...v2.2.1) From edf287e3424355d18a2792481de1cda17bf3648e Mon Sep 17 00:00:00 2001 From: Gert de Pagter Date: Wed, 6 Nov 2024 15:37:16 +0100 Subject: [PATCH 016/102] minor: Add templated types to flush_after (#719) --- src/Persistence/PersistenceManager.php | 6 +++++- src/Persistence/functions.php | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/Persistence/PersistenceManager.php b/src/Persistence/PersistenceManager.php index ed18cd056..88979e6d2 100644 --- a/src/Persistence/PersistenceManager.php +++ b/src/Persistence/PersistenceManager.php @@ -76,7 +76,11 @@ public function save(object $object): object } /** - * @param callable():mixed $callback + * @template T + * + * @param callable():T $callback + * + * @return T */ public function flushAfter(callable $callback): mixed { diff --git a/src/Persistence/functions.php b/src/Persistence/functions.php index 651ce0eaf..b38b608c9 100644 --- a/src/Persistence/functions.php +++ b/src/Persistence/functions.php @@ -149,7 +149,11 @@ function delete(object $object): object } /** - * @param callable():mixed $callback + * @template T + * + * @param callable():T $callback + * + * @return T */ function flush_after(callable $callback): mixed { From efadea8e02b4a8f846039d032c3ec7a199bd0dae Mon Sep 17 00:00:00 2001 From: Andreas Allacher Date: Fri, 22 Nov 2024 15:47:34 +0100 Subject: [PATCH 017/102] docs:fix code blocks not showing up (#723) --- docs/index.rst | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index 751e4b0d1..407f07d3b 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1620,11 +1620,11 @@ You can install it by modifying you ``phpunit.xml.dist``: .. code-block:: xml - - - - - + + + + + .. warning:: @@ -1825,7 +1825,7 @@ This can dramatically improve test speed. The following considerations need to b .. code-block:: yaml - # config/packages/doctrine.yaml + # config/packages/doctrine.yaml when@test: doctrine: dbal: From 6d087842b4c0a1fe69b66f1aab271d9fd307f30f Mon Sep 17 00:00:00 2001 From: Nicolas PHILIPPE Date: Fri, 22 Nov 2024 21:12:34 +0100 Subject: [PATCH 018/102] fix: bug with one to many (#722) --- src/Factory.php | 18 +- src/FactoryCollection.php | 53 ++++- .../Entity/Contact/CascadeContactFactory.php | 3 +- .../Contact/ProxyCascadeContactFactory.php | 3 +- .../Entity/Contact/ProxyContactFactory.php | 2 +- .../Entity/Contact/StandardContactFactory.php | 2 + .../CascadeEntityFactoryRelationshipTest.php | 40 ++-- .../ORM/EntityFactoryRelationshipTestCase.php | 202 ++++++++++-------- ...lymorphicEntityFactoryRelationshipTest.php | 2 +- ...xyCascadeEntityFactoryRelationshipTest.php | 8 +- .../ProxyEntityFactoryRelationshipTest.php | 8 +- ...ProxyEntityFactoryRelationshipTestCase.php | 40 ++-- .../StandardEntityFactoryRelationshipTest.php | 8 +- 13 files changed, 234 insertions(+), 155 deletions(-) diff --git a/src/Factory.php b/src/Factory.php index 37d1c84bb..4707fa70b 100644 --- a/src/Factory.php +++ b/src/Factory.php @@ -202,13 +202,6 @@ protected function normalizeParameters(array $parameters): array */ protected function normalizeParameter(string $field, mixed $value): mixed { - if (\is_array($value)) { - return array_combine( - array_keys($value), - \array_map($this->normalizeParameter(...), array_fill(0, count($value), $field), $value) - ); - } - if ($value instanceof LazyValue) { $value = $value(); } @@ -217,10 +210,21 @@ protected function normalizeParameter(string $field, mixed $value): mixed $value = $value->create(); } + if (FactoryCollection::accepts($value)) { + $value = FactoryCollection::fromFactoriesList($value); + } + if ($value instanceof FactoryCollection) { $value = $this->normalizeCollection($field, $value); } + if (\is_array($value)) { + return array_combine( + array_keys($value), + \array_map($this->normalizeParameter(...), array_fill(0, count($value), $field), $value) + ); + } + return \is_object($value) ? $this->normalizeObject($value) : $value; } diff --git a/src/FactoryCollection.php b/src/FactoryCollection.php index 55a2e702a..7d9d6ee86 100644 --- a/src/FactoryCollection.php +++ b/src/FactoryCollection.php @@ -23,12 +23,53 @@ final class FactoryCollection implements \IteratorAggregate { /** * @param Factory $factory - * @phpstan-param \Closure():iterable $items + * @phpstan-param \Closure():iterable|\Closure():iterable> $items */ private function __construct(public readonly Factory $factory, private \Closure $items) { } + /** + * @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)) { + return false; + } + + if (!$potentialFactories[0] instanceof ObjectFactory) { + return false; + } + + foreach ($potentialFactories as $potentialFactory) { + if (!$potentialFactory instanceof ObjectFactory + || $potentialFactory::class() !== $potentialFactories[0]::class()) { + return false; + } + } + + return true; + } + + /** + * @param array $factories + * + * @return self + * + * @internal + */ + public static function fromFactoriesList(array $factories): self + { + if (!self::accepts($factories)) { + throw new \InvalidArgumentException('All factories must be of the same type.'); + } + + return new self($factories[0], static fn() => $factories); + } + /** * @param Factory $factory * @@ -81,8 +122,14 @@ public function all(): array $factories = []; $i = 1; - foreach (($this->items)() as $attributes) { - $factories[] = $this->factory->with($attributes)->with(['__index' => $i++]); + foreach (($this->items)() as $attributesOrFactory) { + if ($attributesOrFactory instanceof Factory) { + $factories[] = $attributesOrFactory; + + continue; + } + + $factories[] = $this->factory->with($attributesOrFactory)->with(['__index' => $i++]); } return $factories; diff --git a/tests/Fixture/Factories/Entity/Contact/CascadeContactFactory.php b/tests/Fixture/Factories/Entity/Contact/CascadeContactFactory.php index 3b98c679d..e92ac9517 100644 --- a/tests/Fixture/Factories/Entity/Contact/CascadeContactFactory.php +++ b/tests/Fixture/Factories/Entity/Contact/CascadeContactFactory.php @@ -14,6 +14,7 @@ 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; /** * @author Kevin Bond @@ -31,7 +32,7 @@ protected function defaults(): array|callable { return [ 'name' => self::faker()->word(), - // 'category' => CascadeCategoryFactory::new(), + 'category' => CascadeCategoryFactory::new(), 'address' => CascadeAddressFactory::new(), ]; } diff --git a/tests/Fixture/Factories/Entity/Contact/ProxyCascadeContactFactory.php b/tests/Fixture/Factories/Entity/Contact/ProxyCascadeContactFactory.php index 41f93dab8..79faebd2c 100644 --- a/tests/Fixture/Factories/Entity/Contact/ProxyCascadeContactFactory.php +++ b/tests/Fixture/Factories/Entity/Contact/ProxyCascadeContactFactory.php @@ -14,6 +14,7 @@ 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 @@ -31,7 +32,7 @@ protected function defaults(): array|callable { return [ 'name' => self::faker()->word(), - // 'category' => ProxyCategoryFactory::new(), + '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 393714c2a..5029f235c 100644 --- a/tests/Fixture/Factories/Entity/Contact/ProxyContactFactory.php +++ b/tests/Fixture/Factories/Entity/Contact/ProxyContactFactory.php @@ -32,7 +32,7 @@ protected function defaults(): array|callable { return [ 'name' => self::faker()->word(), - // 'category' => ProxyCategoryFactory::new(), + 'category' => ProxyCategoryFactory::new(), 'address' => ProxyAddressFactory::new(), ]; } diff --git a/tests/Fixture/Factories/Entity/Contact/StandardContactFactory.php b/tests/Fixture/Factories/Entity/Contact/StandardContactFactory.php index 7990ce9d8..a8ca2d6c7 100644 --- a/tests/Fixture/Factories/Entity/Contact/StandardContactFactory.php +++ b/tests/Fixture/Factories/Entity/Contact/StandardContactFactory.php @@ -14,6 +14,7 @@ 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 @@ -32,6 +33,7 @@ protected function defaults(): array|callable return [ 'name' => self::faker()->word(), 'address' => StandardAddressFactory::new(), + 'category' => StandardCategoryFactory::new(), ]; } } diff --git a/tests/Integration/ORM/CascadeEntityFactoryRelationshipTest.php b/tests/Integration/ORM/CascadeEntityFactoryRelationshipTest.php index ec00ef40f..a0cc97804 100644 --- a/tests/Integration/ORM/CascadeEntityFactoryRelationshipTest.php +++ b/tests/Integration/ORM/CascadeEntityFactoryRelationshipTest.php @@ -27,15 +27,15 @@ final class CascadeEntityFactoryRelationshipTest extends EntityFactoryRelationsh */ public function ensure_to_one_cascade_relations_are_not_pre_persisted(): void { - $contact = $this->contactFactory() + $contact = self::contactFactory() ->afterInstantiate(function() { - $this->categoryFactory()::repository()->assert()->empty(); - $this->addressFactory()::repository()->assert()->empty(); - $this->tagFactory()::repository()->assert()->empty(); + self::categoryFactory()::repository()->assert()->empty(); + self::addressFactory()::repository()->assert()->empty(); + self::tagFactory()::repository()->assert()->empty(); }) ->create([ - 'tags' => $this->tagFactory()->many(3), - 'category' => $this->categoryFactory(), + 'tags' => self::tagFactory()->many(3), + 'category' => self::categoryFactory(), ]) ; @@ -53,14 +53,14 @@ public function ensure_to_one_cascade_relations_are_not_pre_persisted(): void */ public function ensure_many_to_many_cascade_relations_are_not_pre_persisted(): void { - $tag = $this->tagFactory() + $tag = self::tagFactory() ->afterInstantiate(function() { - $this->categoryFactory()::repository()->assert()->empty(); - $this->addressFactory()::repository()->assert()->empty(); - $this->contactFactory()::repository()->assert()->empty(); + self::categoryFactory()::repository()->assert()->empty(); + self::addressFactory()::repository()->assert()->empty(); + self::contactFactory()::repository()->assert()->empty(); }) ->create([ - 'contacts' => $this->contactFactory()->many(3), + 'contacts' => self::contactFactory()->many(3), ]) ; @@ -76,14 +76,14 @@ public function ensure_many_to_many_cascade_relations_are_not_pre_persisted(): v */ public function ensure_one_to_many_cascade_relations_are_not_pre_persisted(): void { - $category = $this->categoryFactory() + $category = self::categoryFactory() ->afterInstantiate(function() { - $this->contactFactory()::repository()->assert()->empty(); - $this->addressFactory()::repository()->assert()->empty(); - $this->tagFactory()::repository()->assert()->empty(); + self::contactFactory()::repository()->assert()->empty(); + self::addressFactory()::repository()->assert()->empty(); + self::tagFactory()::repository()->assert()->empty(); }) ->create([ - 'contacts' => $this->contactFactory()->many(3), + 'contacts' => self::contactFactory()->many(3), ]) ; @@ -94,22 +94,22 @@ public function ensure_one_to_many_cascade_relations_are_not_pre_persisted(): vo } } - protected function contactFactory(): PersistentObjectFactory + protected static function contactFactory(): PersistentObjectFactory { return CascadeContactFactory::new(); // @phpstan-ignore return.type } - protected function categoryFactory(): PersistentObjectFactory + protected static function categoryFactory(): PersistentObjectFactory { return CascadeCategoryFactory::new(); // @phpstan-ignore return.type } - protected function tagFactory(): PersistentObjectFactory + protected static function tagFactory(): PersistentObjectFactory { return CascadeTagFactory::new(); // @phpstan-ignore return.type } - protected function addressFactory(): PersistentObjectFactory + protected static function addressFactory(): PersistentObjectFactory { return CascadeAddressFactory::new(); // @phpstan-ignore return.type } diff --git a/tests/Integration/ORM/EntityFactoryRelationshipTestCase.php b/tests/Integration/ORM/EntityFactoryRelationshipTestCase.php index a786f217d..487465270 100644 --- a/tests/Integration/ORM/EntityFactoryRelationshipTestCase.php +++ b/tests/Integration/ORM/EntityFactoryRelationshipTestCase.php @@ -13,6 +13,8 @@ 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; @@ -37,12 +39,12 @@ abstract class EntityFactoryRelationshipTestCase extends KernelTestCase */ public function many_to_one(): void { - $contact = $this->contactFactory()::createOne([ - 'category' => $this->categoryFactory(), + $contact = static::contactFactory()::createOne([ + 'category' => static::categoryFactory(), ]); - $this->contactFactory()::repository()->assert()->count(1); - $this->categoryFactory()::repository()->assert()->count(1); + static::contactFactory()::repository()->assert()->count(1); + static::categoryFactory()::repository()->assert()->count(1); $this->assertNotNull($contact->id); $this->assertNotNull($contact->getCategory()?->id); @@ -53,15 +55,15 @@ public function many_to_one(): void */ public function disabling_persistence_cascades_to_children(): void { - $contact = $this->contactFactory()->withoutPersisting()->create([ - 'tags' => $this->tagFactory()->many(3), - 'category' => $this->categoryFactory(), + $contact = static::contactFactory()->withoutPersisting()->create([ + 'tags' => static::tagFactory()->many(3), + 'category' => static::categoryFactory(), ]); - $this->contactFactory()::repository()->assert()->empty(); - $this->categoryFactory()::repository()->assert()->empty(); - $this->tagFactory()::repository()->assert()->empty(); - $this->addressFactory()::repository()->assert()->empty(); + 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); @@ -72,12 +74,12 @@ public function disabling_persistence_cascades_to_children(): void $this->assertNull($tag->id); } - $category = $this->categoryFactory()->withoutPersisting()->create([ - 'contacts' => $this->contactFactory()->many(3), + $category = static::categoryFactory()->withoutPersisting()->create([ + 'contacts' => static::contactFactory()->many(3), ]); - $this->contactFactory()::repository()->assert()->empty(); - $this->categoryFactory()::repository()->assert()->empty(); + static::contactFactory()::repository()->assert()->empty(); + static::categoryFactory()::repository()->assert()->empty(); $this->assertNull($category->id); $this->assertCount(3, $category->getContacts()); @@ -89,17 +91,48 @@ public function disabling_persistence_cascades_to_children(): void /** * @test + * @param FactoryCollection|list> $contacts + * @dataProvider one_to_many_provider */ - public function one_to_many(): void + public function one_to_many(FactoryCollection|array $contacts): void { - $category = $this->categoryFactory()::createOne([ - 'contacts' => $this->contactFactory()->many(3), + $category = static::categoryFactory()::createOne([ + 'contacts' => $contacts, ]); - $this->contactFactory()::repository()->assert()->count(3); - $this->categoryFactory()::repository()->assert()->count(1); + static::contactFactory()::repository()->assert()->count(2); + static::categoryFactory()::repository()->assert()->count(1); $this->assertNotNull($category->id); - $this->assertCount(3, $category->getContacts()); + $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); @@ -111,12 +144,12 @@ public function one_to_many(): void */ public function many_to_many_owning(): void { - $tag = $this->tagFactory()::createOne([ - 'contacts' => $this->contactFactory()->many(3), + $tag = static::tagFactory()::createOne([ + 'contacts' => static::contactFactory()->many(3), ]); - $this->contactFactory()::repository()->assert()->count(3); - $this->tagFactory()::repository()->assert()->count(1); + static::contactFactory()::repository()->assert()->count(3); + static::tagFactory()::repository()->assert()->count(1); $this->assertNotNull($tag->id); foreach ($tag->getContacts() as $contact) { @@ -129,12 +162,12 @@ public function many_to_many_owning(): void */ public function many_to_many_owning_as_array(): void { - $tag = $this->tagFactory()::createOne([ - 'contacts' => [$this->contactFactory(), $this->contactFactory(), $this->contactFactory()], + $tag = static::tagFactory()::createOne([ + 'contacts' => [static::contactFactory(), static::contactFactory(), static::contactFactory()], ]); - $this->contactFactory()::repository()->assert()->count(3); - $this->tagFactory()::repository()->assert()->count(1); + static::contactFactory()::repository()->assert()->count(3); + static::tagFactory()::repository()->assert()->count(1); $this->assertNotNull($tag->id); foreach ($tag->getContacts() as $contact) { @@ -147,12 +180,12 @@ public function many_to_many_owning_as_array(): void */ public function many_to_many_inverse(): void { - $contact = $this->contactFactory()::createOne([ - 'tags' => $this->tagFactory()->many(3), + $contact = static::contactFactory()::createOne([ + 'tags' => static::tagFactory()->many(3), ]); - $this->contactFactory()::repository()->assert()->count(1); - $this->tagFactory()::repository()->assert()->count(3); + static::contactFactory()::repository()->assert()->count(1); + static::tagFactory()::repository()->assert()->count(3); $this->assertNotNull($contact->id); foreach ($contact->getTags() as $tag) { @@ -166,10 +199,10 @@ public function many_to_many_inverse(): void */ public function one_to_one_owning(): void { - $contact = $this->contactFactory()::createOne(); + $contact = static::contactFactory()::createOne(); - $this->contactFactory()::repository()->assert()->count(1); - $this->addressFactory()::repository()->assert()->count(1); + static::contactFactory()::repository()->assert()->count(1); + static::addressFactory()::repository()->assert()->count(1); $this->assertNotNull($contact->id); $this->assertNotNull($contact->getAddress()->id); @@ -188,13 +221,13 @@ public function one_to_one_inverse(): void */ public function many_to_one_unmanaged_raw_entity(): void { - $address = unproxy($this->addressFactory()->create(['city' => 'Some city'])); + $address = unproxy(static::addressFactory()->create(['city' => 'Some city'])); /** @var EntityManagerInterface $em */ $em = self::getContainer()->get(EntityManagerInterface::class); $em->clear(); - $contact = $this->contactFactory()->create(['address' => $address]); + $contact = static::contactFactory()->create(['address' => $address]); $this->assertSame('Some city', $contact->getAddress()->getCity()); } @@ -202,36 +235,43 @@ public function many_to_one_unmanaged_raw_entity(): void /** * @test */ - public function inverse_one_to_many_relationship(): void + public function one_to_many_with_two_relationships_same_entity(): void { - $this->categoryFactory()::assert()->count(0); - $this->contactFactory()::assert()->count(0); - - $this->categoryFactory()->create([ - 'contacts' => [ - $this->contactFactory(), - $this->contactFactory()->create(), - ], + $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->categoryFactory()::assert()->count(1); - $this->contactFactory()::assert()->count(2); + $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(): void + public function one_to_many_with_two_relationships_same_entity_and_adders(): void { - $category = $this->categoryFactory()->create([ - 'contacts' => $this->contactFactory()->many(4), - 'secondaryContacts' => $this->contactFactory()->many(4), + $category = static::categoryFactory()->create([ + 'addContact' => static::contactFactory()->with(['category' => null]), + 'addSecondaryContact' => static::contactFactory()->with(['category' => null]), ]); - $this->assertCount(4, $category->getContacts()); - $this->assertCount(4, $category->getSecondaryContacts()); - $this->contactFactory()::assert()->count(8); - $this->categoryFactory()::assert()->count(1); + $this->assertCount(1, $category->getContacts()); + $this->assertCount(1, $category->getSecondaryContacts()); + static::contactFactory()::assert()->count(2); + static::categoryFactory()::assert()->count(1); } /** @@ -239,17 +279,17 @@ public function one_to_many_with_two_relationships_same_entity(): void */ public function inverse_many_to_many_with_two_relationships_same_entity(): void { - $this->tagFactory()::assert()->count(0); + static::tagFactory()::assert()->count(0); - $tag = $this->tagFactory()->create([ - 'contacts' => $this->contactFactory()->many(3), - 'secondaryContacts' => $this->contactFactory()->many(3), + $tag = static::tagFactory()->create([ + 'contacts' => static::contactFactory()->many(3), + 'secondaryContacts' => static::contactFactory()->many(3), ]); $this->assertCount(3, $tag->getContacts()); $this->assertCount(3, $tag->getSecondaryContacts()); - $this->tagFactory()::assert()->count(1); - $this->contactFactory()::assert()->count(6); + static::tagFactory()::assert()->count(1); + static::contactFactory()::assert()->count(6); } /** @@ -257,39 +297,23 @@ public function inverse_many_to_many_with_two_relationships_same_entity(): void */ public function can_use_adder_as_attributes(): void { - $category = $this->categoryFactory()->create([ - 'addContact' => $this->contactFactory()->with(['name' => 'foo']), + $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 one_to_many_with_two_relationships_same_entity_and_adders(): void - { - $category = $this->categoryFactory()->create([ - 'addContact' => $this->contactFactory(), - 'addSecondaryContact' => $this->contactFactory(), - ]); - - $this->assertCount(1, $category->getContacts()); - $this->assertCount(1, $category->getSecondaryContacts()); - $this->contactFactory()::assert()->count(2); - $this->categoryFactory()::assert()->count(1); - } - /** * @test */ public function forced_one_to_many_with_doctrine_collection_type(): void { - $category = $this->categoryFactory() + $category = static::categoryFactory() ->instantiateWith(Instantiator::withConstructor()->alwaysForce()) ->create([ - 'contacts' => $this->contactFactory()->many(2), + 'contacts' => static::contactFactory()->many(2), ]) ; @@ -297,27 +321,27 @@ public function forced_one_to_many_with_doctrine_collection_type(): void foreach ($category->getContacts() as $contact) { self::assertSame(unproxy($category), $contact->getCategory()); } - $this->contactFactory()::assert()->count(2); - $this->categoryFactory()::assert()->count(1); + static::contactFactory()::assert()->count(2); + static::categoryFactory()::assert()->count(1); } /** * @return PersistentObjectFactory */ - abstract protected function contactFactory(): PersistentObjectFactory; + abstract protected static function contactFactory(): PersistentObjectFactory; /** * @return PersistentObjectFactory */ - abstract protected function categoryFactory(): PersistentObjectFactory; + abstract protected static function categoryFactory(): PersistentObjectFactory; /** * @return PersistentObjectFactory */ - abstract protected function tagFactory(): PersistentObjectFactory; + abstract protected static function tagFactory(): PersistentObjectFactory; /** * @return PersistentObjectFactory
*/ - abstract protected function addressFactory(): PersistentObjectFactory; + abstract protected static function addressFactory(): PersistentObjectFactory; } diff --git a/tests/Integration/ORM/PolymorphicEntityFactoryRelationshipTest.php b/tests/Integration/ORM/PolymorphicEntityFactoryRelationshipTest.php index 92607a6b7..46b54c83b 100644 --- a/tests/Integration/ORM/PolymorphicEntityFactoryRelationshipTest.php +++ b/tests/Integration/ORM/PolymorphicEntityFactoryRelationshipTest.php @@ -19,7 +19,7 @@ */ class PolymorphicEntityFactoryRelationshipTest extends StandardEntityFactoryRelationshipTest { - protected function contactFactory(): PersistentObjectFactory + 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 index e466a2465..3c5a401fe 100644 --- a/tests/Integration/ORM/ProxyCascadeEntityFactoryRelationshipTest.php +++ b/tests/Integration/ORM/ProxyCascadeEntityFactoryRelationshipTest.php @@ -22,22 +22,22 @@ */ final class ProxyCascadeEntityFactoryRelationshipTest extends ProxyEntityFactoryRelationshipTestCase { - protected function contactFactory(): PersistentObjectFactory + protected static function contactFactory(): PersistentObjectFactory { return ProxyCascadeContactFactory::new(); // @phpstan-ignore return.type } - protected function categoryFactory(): PersistentObjectFactory + protected static function categoryFactory(): PersistentObjectFactory { return ProxyCascadeCategoryFactory::new(); // @phpstan-ignore return.type } - protected function tagFactory(): PersistentObjectFactory + protected static function tagFactory(): PersistentObjectFactory { return ProxyCascadeTagFactory::new(); // @phpstan-ignore return.type } - protected function addressFactory(): PersistentObjectFactory + 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 index a56852831..064d58b9f 100644 --- a/tests/Integration/ORM/ProxyEntityFactoryRelationshipTest.php +++ b/tests/Integration/ORM/ProxyEntityFactoryRelationshipTest.php @@ -22,22 +22,22 @@ */ final class ProxyEntityFactoryRelationshipTest extends ProxyEntityFactoryRelationshipTestCase { - protected function contactFactory(): PersistentObjectFactory + protected static function contactFactory(): PersistentObjectFactory { return ProxyContactFactory::new(); // @phpstan-ignore return.type } - protected function categoryFactory(): PersistentObjectFactory + protected static function categoryFactory(): PersistentObjectFactory { return ProxyCategoryFactory::new(); // @phpstan-ignore return.type } - protected function tagFactory(): PersistentObjectFactory + protected static function tagFactory(): PersistentObjectFactory { return ProxyTagFactory::new(); // @phpstan-ignore return.type } - protected function addressFactory(): PersistentObjectFactory + protected static function addressFactory(): PersistentObjectFactory { return ProxyAddressFactory::new(); // @phpstan-ignore return.type } diff --git a/tests/Integration/ORM/ProxyEntityFactoryRelationshipTestCase.php b/tests/Integration/ORM/ProxyEntityFactoryRelationshipTestCase.php index 400d777bc..d9217bbe8 100644 --- a/tests/Integration/ORM/ProxyEntityFactoryRelationshipTestCase.php +++ b/tests/Integration/ORM/ProxyEntityFactoryRelationshipTestCase.php @@ -40,21 +40,21 @@ abstract class ProxyEntityFactoryRelationshipTestCase extends EntityFactoryRelat */ public function doctrine_proxies_are_converted_to_foundry_proxies(): void { - $this->contactFactory()->create(['category' => $this->categoryFactory()]); + static::contactFactory()->create(['category' => static::categoryFactory()]); // clear the em so nothing is tracked self::getContainer()->get(EntityManagerInterface::class)->clear(); // @phpstan-ignore method.nonObject // load a random Contact which causes the em to track a "doctrine proxy" for category - $this->contactFactory()::random(); + static::contactFactory()::random(); // load a random Category which should be a "doctrine proxy" - $category = $this->categoryFactory()::random(); + $category = static::categoryFactory()::random(); // ensure the category is a "doctrine proxy" and a Category $this->assertInstanceOf(Proxy::class, $category); $this->assertInstanceOf(DoctrineProxy::class, $category->_real()); - $this->assertInstanceOf($this->categoryFactory()::class(), $category); + $this->assertInstanceOf(static::categoryFactory()::class(), $category); } /** @@ -62,13 +62,13 @@ public function doctrine_proxies_are_converted_to_foundry_proxies(): void */ public function it_can_add_proxy_to_many_to_one(): void { - $contact = $this->contactFactory()->create(); + $contact = static::contactFactory()->create(); - $contact->setCategory($category = $this->categoryFactory()->create()); + $contact->setCategory($category = static::categoryFactory()->create()); $contact->_save(); - $this->contactFactory()::assert()->count(1); - $this->contactFactory()::assert()->exists(['category' => $category]); + static::contactFactory()::assert()->count(1); + static::contactFactory()::assert()->exists(['category' => $category]); } /** @@ -76,13 +76,13 @@ public function it_can_add_proxy_to_many_to_one(): void */ public function it_can_add_proxy_to_one_to_many(): void { - $contact = $this->contactFactory()->create(); + $contact = static::contactFactory()->create(); - $contact->addTag($this->tagFactory()->create()); + $contact->addTag(static::tagFactory()->create()); $contact->_save(); - $this->contactFactory()::assert()->count(1); - $tag = $this->tagFactory()::first(); + static::contactFactory()::assert()->count(1); + $tag = static::tagFactory()::first(); self::assertContains($contact->_real(), $tag->getContacts()); } @@ -91,10 +91,10 @@ public function it_can_add_proxy_to_one_to_many(): void */ public function can_assert_persisted(): void { - $this->contactFactory()->create()->_assertPersisted(); + static::contactFactory()->create()->_assertPersisted(); - Assert::that(function(): void { $this->contactFactory()->withoutPersisting()->create()->_assertPersisted(); }) - ->throws(AssertionFailedError::class, \sprintf('%s is not persisted.', $this->contactFactory()::class())) + Assert::that(function(): void { static::contactFactory()->withoutPersisting()->create()->_assertPersisted(); }) + ->throws(AssertionFailedError::class, \sprintf('%s is not persisted.', static::contactFactory()::class())) ; } @@ -103,10 +103,10 @@ public function can_assert_persisted(): void */ public function can_assert_not_persisted(): void { - $this->contactFactory()->withoutPersisting()->create()->_assertNotPersisted(); + static::contactFactory()->withoutPersisting()->create()->_assertNotPersisted(); - Assert::that(function(): void { $this->contactFactory()->create()->_assertNotPersisted(); }) - ->throws(AssertionFailedError::class, \sprintf('%s is persisted but it should not be.', $this->contactFactory()::class())) + Assert::that(function(): void { static::contactFactory()->create()->_assertNotPersisted(); }) + ->throws(AssertionFailedError::class, \sprintf('%s is persisted but it should not be.', static::contactFactory()::class())) ; } @@ -115,7 +115,7 @@ public function can_assert_not_persisted(): void */ public function can_remove_and_assert_not_persisted(): void { - $this->contactFactory() + static::contactFactory() ->create() ->_assertPersisted() ->_delete() @@ -128,7 +128,7 @@ public function can_remove_and_assert_not_persisted(): void */ public function cannot_use_assert_persisted_when_entity_has_changes(): void { - $contact = $this->contactFactory()->create(); + $contact = static::contactFactory()->create(); $contact->setName('foo'); $this->expectException(RefreshObjectFailed::class); diff --git a/tests/Integration/ORM/StandardEntityFactoryRelationshipTest.php b/tests/Integration/ORM/StandardEntityFactoryRelationshipTest.php index 5189be1cf..acd5b57ae 100644 --- a/tests/Integration/ORM/StandardEntityFactoryRelationshipTest.php +++ b/tests/Integration/ORM/StandardEntityFactoryRelationshipTest.php @@ -22,22 +22,22 @@ */ class StandardEntityFactoryRelationshipTest extends EntityFactoryRelationshipTestCase { - protected function contactFactory(): PersistentObjectFactory + protected static function contactFactory(): PersistentObjectFactory { return StandardContactFactory::new(); // @phpstan-ignore return.type } - protected function categoryFactory(): PersistentObjectFactory + protected static function categoryFactory(): PersistentObjectFactory { return StandardCategoryFactory::new(); // @phpstan-ignore return.type } - protected function tagFactory(): PersistentObjectFactory + protected static function tagFactory(): PersistentObjectFactory { return StandardTagFactory::new(); // @phpstan-ignore return.type } - protected function addressFactory(): PersistentObjectFactory + protected static function addressFactory(): PersistentObjectFactory { return StandardAddressFactory::new(); // @phpstan-ignore return.type } From f48ffd115e06d731c464ddef88c0e67e4d26b7be Mon Sep 17 00:00:00 2001 From: Nicolas PHILIPPE Date: Fri, 22 Nov 2024 21:15:36 +0100 Subject: [PATCH 019/102] fix: can create inversed one to one (#659) --- src/ORM/OrmV2PersistenceStrategy.php | 46 +++++++++++++------ src/ORM/OrmV3PersistenceStrategy.php | 40 ++++++++++------ src/Persistence/PersistentObjectFactory.php | 21 ++++++++- src/Persistence/RelationshipMetadata.php | 1 + tests/Fixture/Entity/Address.php | 12 +++++ .../Fixture/Entity/Address/CascadeAddress.php | 4 ++ .../Entity/Address/StandardAddress.php | 4 ++ .../Fixture/Entity/Contact/CascadeContact.php | 2 +- .../ORM/EntityFactoryRelationshipTestCase.php | 24 ++++++---- 9 files changed, 117 insertions(+), 37 deletions(-) diff --git a/src/ORM/OrmV2PersistenceStrategy.php b/src/ORM/OrmV2PersistenceStrategy.php index 649110c82..c19195e4b 100644 --- a/src/ORM/OrmV2PersistenceStrategy.php +++ b/src/ORM/OrmV2PersistenceStrategy.php @@ -31,27 +31,47 @@ public function relationshipMetadata(string $parent, string $child, string $fiel $association = $this->getAssociationMapping($parent, $field); - if (null === $association) { - $inversedAssociation = $this->getAssociationMapping($child, $field); + if ($association) { + return new RelationshipMetadata( + isCascadePersist: $association['isCascadePersist'], + inverseField: $metadata->isSingleValuedAssociation($association['fieldName']) ? $association['fieldName'] : null, + isCollection: $metadata->isCollectionValuedAssociation($association['fieldName']), + ); + } - if (null === $inversedAssociation || !$metadata instanceof ClassMetadataInfo) { - return null; - } + $inversedAssociation = $this->getAssociationMapping($child, $field); - if (!\is_a($parent, $inversedAssociation['targetEntity'], allow_string: true)) { // is_a() handles inheritance as well - throw new \LogicException("Cannot find correct association named \"{$field}\" between classes [parent: \"{$parent}\", child: \"{$child}\"]"); - } + if (null === $inversedAssociation || !$metadata instanceof ClassMetadataInfo) { + return null; + } - if (ClassMetadataInfo::ONE_TO_MANY !== $inversedAssociation['type'] || !isset($inversedAssociation['mappedBy'])) { - return null; - } + if (!\is_a( + $parent, + $inversedAssociation['targetEntity'], + allow_string: true + )) { // is_a() handles inheritance as well + throw new \LogicException( + "Cannot find correct association named \"{$field}\" between classes [parent: \"{$parent}\", child: \"{$child}\"]" + ); + } - $association = $metadata->getAssociationMapping($inversedAssociation['mappedBy']); + if (!in_array( + $inversedAssociation['type'], + [ClassMetadataInfo::ONE_TO_MANY, ClassMetadataInfo::ONE_TO_ONE], + true + ) + || !isset($inversedAssociation['mappedBy']) + ) { + return null; } + $association = $metadata->getAssociationMapping($inversedAssociation['mappedBy']); + $inversedAssociationMetadata = $this->classMetadata($inversedAssociation['sourceEntity']); + return new RelationshipMetadata( - isCascadePersist: $association['isCascadePersist'], + isCascadePersist: $inversedAssociation['isCascadePersist'], inverseField: $metadata->isSingleValuedAssociation($association['fieldName']) ? $association['fieldName'] : null, + isCollection: $inversedAssociationMetadata->isCollectionValuedAssociation($inversedAssociation['fieldName']), ); } diff --git a/src/ORM/OrmV3PersistenceStrategy.php b/src/ORM/OrmV3PersistenceStrategy.php index 047208da2..ea40e984c 100644 --- a/src/ORM/OrmV3PersistenceStrategy.php +++ b/src/ORM/OrmV3PersistenceStrategy.php @@ -17,6 +17,7 @@ use Doctrine\ORM\Mapping\ClassMetadata; use Doctrine\ORM\Mapping\InverseSideMapping; use Doctrine\ORM\Mapping\MappingException as ORMMappingException; +use Doctrine\ORM\Mapping\ToManyAssociationMapping; use Doctrine\Persistence\Mapping\MappingException; use Zenstruck\Foundry\Persistence\RelationshipMetadata; @@ -28,27 +29,40 @@ public function relationshipMetadata(string $parent, string $child, string $fiel $association = $this->getAssociationMapping($parent, $field); - if (null === $association) { - $inversedAssociation = $this->getAssociationMapping($child, $field); + if ($association) { + return new RelationshipMetadata( + isCascadePersist: $association->isCascadePersist(), + inverseField: $metadata->isSingleValuedAssociation($association->fieldName) ? $association->fieldName : null, + isCollection: $association instanceof ToManyAssociationMapping + ); + } - if (null === $inversedAssociation || !$metadata instanceof ClassMetadata) { - return null; - } + $inversedAssociation = $this->getAssociationMapping($child, $field); - if (!\is_a($parent, $inversedAssociation->targetEntity, allow_string: true)) { // is_a() handles inheritance as well - throw new \LogicException("Cannot find correct association named \"{$field}\" between classes [parent: \"{$parent}\", child: \"{$child}\"]"); - } + if (null === $inversedAssociation || !$metadata instanceof ClassMetadata) { + return null; + } - if (!$inversedAssociation instanceof InverseSideMapping) { - return null; - } + if (!\is_a( + $parent, + $inversedAssociation->targetEntity, + allow_string: true + )) { // is_a() handles inheritance as well + throw new \LogicException( + "Cannot find correct association named \"{$field}\" between classes [parent: \"{$parent}\", child: \"{$child}\"]" + ); + } - $association = $metadata->getAssociationMapping($inversedAssociation->mappedBy); + if (!$inversedAssociation instanceof InverseSideMapping) { + return null; } + $association = $metadata->getAssociationMapping($inversedAssociation->mappedBy); + return new RelationshipMetadata( - isCascadePersist: $association->isCascadePersist(), + isCascadePersist: $inversedAssociation->isCascadePersist(), inverseField: $metadata->isSingleValuedAssociation($association->fieldName) ? $association->fieldName : null, + isCollection: $inversedAssociation instanceof ToManyAssociationMapping ); } diff --git a/src/Persistence/PersistentObjectFactory.php b/src/Persistence/PersistentObjectFactory.php index 4198adddc..a3eebf1ef 100644 --- a/src/Persistence/PersistentObjectFactory.php +++ b/src/Persistence/PersistentObjectFactory.php @@ -264,8 +264,25 @@ protected function normalizeParameter(string $field, mixed $value): mixed $value->persist = $this->persist; // todo - breaks immutability } - if ($value instanceof self && Configuration::instance()->persistence()->relationshipMetadata(static::class(), $value::class(), $field)?->isCascadePersist) { - $value->persist = false; + if ($value instanceof self) { + $pm = Configuration::instance()->persistence(); + + $relationshipMetadata = $pm->relationshipMetadata($value::class(), static::class(), $field); + + // handle inversed OneToOne + if ($relationshipMetadata && !$relationshipMetadata->isCollection && $inverseField = $relationshipMetadata->inverseField) { + $this->tempAfterPersist[] = static function(object $object) use ($value, $inverseField, $pm) { + $value->create([$inverseField => $object]); + $pm->refresh($object); + }; + + // creation delegated to afterPersist hook - return empty array here + return null; + } + + if (Configuration::instance()->persistence()->relationshipMetadata(static::class(), $value::class(), $field)?->isCascadePersist) { + $value->persist = false; + } } return unproxy(parent::normalizeParameter($field, $value)); diff --git a/src/Persistence/RelationshipMetadata.php b/src/Persistence/RelationshipMetadata.php index 6ea69f926..fb9829ab4 100644 --- a/src/Persistence/RelationshipMetadata.php +++ b/src/Persistence/RelationshipMetadata.php @@ -21,6 +21,7 @@ final class RelationshipMetadata public function __construct( public readonly bool $isCascadePersist, public readonly ?string $inverseField, + public readonly bool $isCollection, ) { } } diff --git a/tests/Fixture/Entity/Address.php b/tests/Fixture/Entity/Address.php index 9cac62894..b8798067c 100644 --- a/tests/Fixture/Entity/Address.php +++ b/tests/Fixture/Entity/Address.php @@ -20,6 +20,8 @@ #[ORM\MappedSuperclass] abstract class Address extends Base { + protected Contact|null $contact = null; + #[ORM\Column(length: 255)] private string $city; @@ -28,6 +30,16 @@ public function __construct(string $city) $this->city = $city; } + public function getContact(): Contact|null + { + return $this->contact; + } + + public function setContact(Contact|null $contact): void + { + $this->contact = $contact; + } + public function getCity(): string { return $this->city; diff --git a/tests/Fixture/Entity/Address/CascadeAddress.php b/tests/Fixture/Entity/Address/CascadeAddress.php index 4eb75e627..1f6a9eda7 100644 --- a/tests/Fixture/Entity/Address/CascadeAddress.php +++ b/tests/Fixture/Entity/Address/CascadeAddress.php @@ -13,6 +13,8 @@ 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 @@ -20,4 +22,6 @@ #[ORM\Entity] class CascadeAddress extends Address { + #[ORM\OneToOne(targetEntity: CascadeContact::class, mappedBy: 'address', cascade: ['persist', 'remove'])] + protected Contact|null $contact = null; } diff --git a/tests/Fixture/Entity/Address/StandardAddress.php b/tests/Fixture/Entity/Address/StandardAddress.php index 5c64946db..9ef5e7d69 100644 --- a/tests/Fixture/Entity/Address/StandardAddress.php +++ b/tests/Fixture/Entity/Address/StandardAddress.php @@ -13,6 +13,8 @@ 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 @@ -20,4 +22,6 @@ #[ORM\Entity] class StandardAddress extends Address { + #[ORM\OneToOne(targetEntity: StandardContact::class, mappedBy: 'address')] + protected Contact|null $contact = null; } diff --git a/tests/Fixture/Entity/Contact/CascadeContact.php b/tests/Fixture/Entity/Contact/CascadeContact.php index f0f4f1357..21b58e3f7 100644 --- a/tests/Fixture/Entity/Contact/CascadeContact.php +++ b/tests/Fixture/Entity/Contact/CascadeContact.php @@ -40,7 +40,7 @@ class CascadeContact extends Contact #[ORM\JoinTable(name: 'category_tag_cascade_secondary')] protected Collection $secondaryTags; - #[ORM\OneToOne(targetEntity: CascadeAddress::class, cascade: ['persist', 'remove'])] + #[ORM\OneToOne(targetEntity: CascadeAddress::class, inversedBy: 'contact', cascade: ['persist', 'remove'])] #[ORM\JoinColumn(nullable: false)] protected Address $address; } diff --git a/tests/Integration/ORM/EntityFactoryRelationshipTestCase.php b/tests/Integration/ORM/EntityFactoryRelationshipTestCase.php index 487465270..2e7c05e87 100644 --- a/tests/Integration/ORM/EntityFactoryRelationshipTestCase.php +++ b/tests/Integration/ORM/EntityFactoryRelationshipTestCase.php @@ -208,14 +208,6 @@ public function one_to_one_owning(): void $this->assertNotNull($contact->getAddress()->id); } - /** - * @test - */ - public function one_to_one_inverse(): void - { - $this->markTestSkipped('Not supported. Should it be?'); - } - /** * @test */ @@ -292,6 +284,22 @@ public function inverse_many_to_many_with_two_relationships_same_entity(): void 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 */ From 8cbee90864f04bf1234efaf34d5f1eb9bba8608d Mon Sep 17 00:00:00 2001 From: kbond Date: Fri, 22 Nov 2024 20:18:12 +0000 Subject: [PATCH 020/102] bot: fix cs [skip ci] --- src/ORM/OrmV2PersistenceStrategy.php | 14 ++++++-------- src/ORM/OrmV3PersistenceStrategy.php | 4 +--- tests/Fixture/Entity/Address.php | 6 +++--- tests/Fixture/Entity/Address/CascadeAddress.php | 2 +- tests/Fixture/Entity/Address/StandardAddress.php | 2 +- 5 files changed, 12 insertions(+), 16 deletions(-) diff --git a/src/ORM/OrmV2PersistenceStrategy.php b/src/ORM/OrmV2PersistenceStrategy.php index c19195e4b..885b2b883 100644 --- a/src/ORM/OrmV2PersistenceStrategy.php +++ b/src/ORM/OrmV2PersistenceStrategy.php @@ -50,16 +50,14 @@ public function relationshipMetadata(string $parent, string $child, string $fiel $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}\"]" - ); + throw new \LogicException("Cannot find correct association named \"{$field}\" between classes [parent: \"{$parent}\", child: \"{$child}\"]"); } - if (!in_array( - $inversedAssociation['type'], - [ClassMetadataInfo::ONE_TO_MANY, ClassMetadataInfo::ONE_TO_ONE], - true - ) + if (!\in_array( + $inversedAssociation['type'], + [ClassMetadataInfo::ONE_TO_MANY, ClassMetadataInfo::ONE_TO_ONE], + true + ) || !isset($inversedAssociation['mappedBy']) ) { return null; diff --git a/src/ORM/OrmV3PersistenceStrategy.php b/src/ORM/OrmV3PersistenceStrategy.php index ea40e984c..93de0b814 100644 --- a/src/ORM/OrmV3PersistenceStrategy.php +++ b/src/ORM/OrmV3PersistenceStrategy.php @@ -48,9 +48,7 @@ public function relationshipMetadata(string $parent, string $child, string $fiel $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}\"]" - ); + throw new \LogicException("Cannot find correct association named \"{$field}\" between classes [parent: \"{$parent}\", child: \"{$child}\"]"); } if (!$inversedAssociation instanceof InverseSideMapping) { diff --git a/tests/Fixture/Entity/Address.php b/tests/Fixture/Entity/Address.php index b8798067c..e7ea0849e 100644 --- a/tests/Fixture/Entity/Address.php +++ b/tests/Fixture/Entity/Address.php @@ -20,7 +20,7 @@ #[ORM\MappedSuperclass] abstract class Address extends Base { - protected Contact|null $contact = null; + protected ?Contact $contact = null; #[ORM\Column(length: 255)] private string $city; @@ -30,12 +30,12 @@ public function __construct(string $city) $this->city = $city; } - public function getContact(): Contact|null + public function getContact(): ?Contact { return $this->contact; } - public function setContact(Contact|null $contact): void + public function setContact(?Contact $contact): void { $this->contact = $contact; } diff --git a/tests/Fixture/Entity/Address/CascadeAddress.php b/tests/Fixture/Entity/Address/CascadeAddress.php index 1f6a9eda7..06fa8b613 100644 --- a/tests/Fixture/Entity/Address/CascadeAddress.php +++ b/tests/Fixture/Entity/Address/CascadeAddress.php @@ -23,5 +23,5 @@ class CascadeAddress extends Address { #[ORM\OneToOne(targetEntity: CascadeContact::class, mappedBy: 'address', cascade: ['persist', 'remove'])] - protected Contact|null $contact = null; + protected ?Contact $contact = null; } diff --git a/tests/Fixture/Entity/Address/StandardAddress.php b/tests/Fixture/Entity/Address/StandardAddress.php index 9ef5e7d69..45522e234 100644 --- a/tests/Fixture/Entity/Address/StandardAddress.php +++ b/tests/Fixture/Entity/Address/StandardAddress.php @@ -23,5 +23,5 @@ class StandardAddress extends Address { #[ORM\OneToOne(targetEntity: StandardContact::class, mappedBy: 'address')] - protected Contact|null $contact = null; + protected ?Contact $contact = null; } From 0e7ac6ffda4ea3b41e234939d9fbf62790f1304b Mon Sep 17 00:00:00 2001 From: Simon <1218015+simondaigre@users.noreply.github.com> Date: Mon, 2 Dec 2024 09:10:08 +0100 Subject: [PATCH 021/102] docs: Fix Story phpdocs (#727) * Update index.rst * Update index.rst Co-authored-by: Nicolas PHILIPPE --------- Co-authored-by: Nicolas PHILIPPE --- docs/index.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/index.rst b/docs/index.rst index 407f07d3b..26b7ec234 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -2106,10 +2106,11 @@ Later, you can access the story's state when creating other fixtures: namespace App\Story; use App\Factory\CategoryFactory; + use Zenstruck\Foundry\Persistence\Proxy; use Zenstruck\Foundry\Story; /** - * @method static Category php() + * @method static Category&Proxy php() */ final class CategoryStory extends Story { From c5d0bdd26e34efe0adf492ad2469ad005f6f8ba8 Mon Sep 17 00:00:00 2001 From: Nicolas PHILIPPE Date: Wed, 4 Dec 2024 13:58:14 +0100 Subject: [PATCH 022/102] fix: can create inversed one to one with non nullable (#726) --- src/Persistence/PersistentObjectFactory.php | 18 +++++++++++--- .../InverseSide.php | 24 +++++++++++++++++++ .../OwningSide.php | 22 +++++++++++++++++ .../ORM/EdgeCasesRelationshipTest.php | 18 ++++++++++++++ 4 files changed, 79 insertions(+), 3 deletions(-) create mode 100644 tests/Fixture/Entity/EdgeCases/InversedOneToOneWithNonNullableOwning/InverseSide.php create mode 100644 tests/Fixture/Entity/EdgeCases/InversedOneToOneWithNonNullableOwning/OwningSide.php diff --git a/src/Persistence/PersistentObjectFactory.php b/src/Persistence/PersistentObjectFactory.php index a3eebf1ef..8b3b9cac4 100644 --- a/src/Persistence/PersistentObjectFactory.php +++ b/src/Persistence/PersistentObjectFactory.php @@ -22,6 +22,8 @@ use Zenstruck\Foundry\Persistence\Exception\NotEnoughObjects; use Zenstruck\Foundry\Persistence\Exception\RefreshObjectFailed; +use function Zenstruck\Foundry\get; + /** * @author Kevin Bond * @@ -271,13 +273,23 @@ protected function normalizeParameter(string $field, mixed $value): mixed // handle inversed OneToOne if ($relationshipMetadata && !$relationshipMetadata->isCollection && $inverseField = $relationshipMetadata->inverseField) { - $this->tempAfterPersist[] = static function(object $object) use ($value, $inverseField, $pm) { + // 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 + } }; - // creation delegated to afterPersist hook - return empty array here - return null; + return $inversedObject; } if (Configuration::instance()->persistence()->relationshipMetadata(static::class(), $value::class(), $field)?->isCascadePersist) { diff --git a/tests/Fixture/Entity/EdgeCases/InversedOneToOneWithNonNullableOwning/InverseSide.php b/tests/Fixture/Entity/EdgeCases/InversedOneToOneWithNonNullableOwning/InverseSide.php new file mode 100644 index 000000000..97e1a7a3c --- /dev/null +++ b/tests/Fixture/Entity/EdgeCases/InversedOneToOneWithNonNullableOwning/InverseSide.php @@ -0,0 +1,24 @@ + + */ +#[ORM\Entity] +class InverseSide +{ + #[ORM\Id] + #[ORM\Column] + #[ORM\GeneratedValue(strategy: 'AUTO')] + public ?int $id = null; + + public function __construct( + #[ORM\OneToOne(mappedBy: 'inverseSide')] + public OwningSide $owningSide + ) {} +} diff --git a/tests/Fixture/Entity/EdgeCases/InversedOneToOneWithNonNullableOwning/OwningSide.php b/tests/Fixture/Entity/EdgeCases/InversedOneToOneWithNonNullableOwning/OwningSide.php new file mode 100644 index 000000000..53b0783d8 --- /dev/null +++ b/tests/Fixture/Entity/EdgeCases/InversedOneToOneWithNonNullableOwning/OwningSide.php @@ -0,0 +1,22 @@ + + */ +#[ORM\Entity] +class OwningSide +{ + #[ORM\Id] + #[ORM\Column] + #[ORM\GeneratedValue(strategy: 'AUTO')] + public ?int $id = null; + + #[ORM\OneToOne(inversedBy: 'owningSide')] + public InverseSide|null $inverseSide = null; +} diff --git a/tests/Integration/ORM/EdgeCasesRelationshipTest.php b/tests/Integration/ORM/EdgeCasesRelationshipTest.php index 621fafb96..6f35f1d3e 100644 --- a/tests/Integration/ORM/EdgeCasesRelationshipTest.php +++ b/tests/Integration/ORM/EdgeCasesRelationshipTest.php @@ -13,6 +13,7 @@ namespace Zenstruck\Foundry\Tests\Integration\ORM; +use Zenstruck\Foundry\Tests\Fixture\Entity\EdgeCases\InversedOneToOneWithNonNullableOwning; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; use Zenstruck\Foundry\Persistence\PersistentObjectFactory; use Zenstruck\Foundry\Persistence\Proxy; @@ -25,6 +26,7 @@ use Zenstruck\Foundry\Tests\Fixture\Stories\GlobalStory; use Zenstruck\Foundry\Tests\Integration\RequiresORM; +use function Zenstruck\Foundry\factory; use function Zenstruck\Foundry\Persistence\flush_after; use function Zenstruck\Foundry\Persistence\persistent_factory; use function Zenstruck\Foundry\Persistence\proxy_factory; @@ -106,6 +108,22 @@ public static function richDomainMandatoryRelationshipFactoryProvider(): iterabl ]; } + /** + * @test + */ + public function inverse_one_to_one_with_non_nullable_inverse_side(): void + { + $owningSideFactory = persistent_factory(InversedOneToOneWithNonNullableOwning\OwningSide::class); + $inverseSideFactory = persistent_factory(InversedOneToOneWithNonNullableOwning\InverseSide::class); + + $inverseSide = $inverseSideFactory->create(['owningSide' => $owningSideFactory]); + + $owningSideFactory::assert()->count(1); + $inverseSideFactory::assert()->count(1); + + self::assertSame($inverseSide, $inverseSide->owningSide->inverseSide); + } + /** * @test */ From 17dd644944842b2ab693e4f705e097a42257e8b9 Mon Sep 17 00:00:00 2001 From: nikophil Date: Wed, 4 Dec 2024 13:00:09 +0000 Subject: [PATCH 023/102] bot: fix cs [skip ci] --- .../InverseSide.php | 14 ++++++++++++-- .../OwningSide.php | 11 ++++++++++- .../Integration/ORM/EdgeCasesRelationshipTest.php | 3 +-- 3 files changed, 23 insertions(+), 5 deletions(-) diff --git a/tests/Fixture/Entity/EdgeCases/InversedOneToOneWithNonNullableOwning/InverseSide.php b/tests/Fixture/Entity/EdgeCases/InversedOneToOneWithNonNullableOwning/InverseSide.php index 97e1a7a3c..82ab4e5a3 100644 --- a/tests/Fixture/Entity/EdgeCases/InversedOneToOneWithNonNullableOwning/InverseSide.php +++ b/tests/Fixture/Entity/EdgeCases/InversedOneToOneWithNonNullableOwning/InverseSide.php @@ -2,6 +2,15 @@ declare(strict_types=1); +/* + * This file is part of the zenstruck/foundry package. + * + * (c) Kevin Bond + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Zenstruck\Foundry\Tests\Fixture\Entity\EdgeCases\InversedOneToOneWithNonNullableOwning; use Doctrine\ORM\Mapping as ORM; @@ -19,6 +28,7 @@ class InverseSide public function __construct( #[ORM\OneToOne(mappedBy: 'inverseSide')] - public OwningSide $owningSide - ) {} + public OwningSide $owningSide, + ) { + } } diff --git a/tests/Fixture/Entity/EdgeCases/InversedOneToOneWithNonNullableOwning/OwningSide.php b/tests/Fixture/Entity/EdgeCases/InversedOneToOneWithNonNullableOwning/OwningSide.php index 53b0783d8..2c8c28cb6 100644 --- a/tests/Fixture/Entity/EdgeCases/InversedOneToOneWithNonNullableOwning/OwningSide.php +++ b/tests/Fixture/Entity/EdgeCases/InversedOneToOneWithNonNullableOwning/OwningSide.php @@ -2,6 +2,15 @@ declare(strict_types=1); +/* + * This file is part of the zenstruck/foundry package. + * + * (c) Kevin Bond + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Zenstruck\Foundry\Tests\Fixture\Entity\EdgeCases\InversedOneToOneWithNonNullableOwning; use Doctrine\ORM\Mapping as ORM; @@ -18,5 +27,5 @@ class OwningSide public ?int $id = null; #[ORM\OneToOne(inversedBy: 'owningSide')] - public InverseSide|null $inverseSide = null; + public ?InverseSide $inverseSide = null; } diff --git a/tests/Integration/ORM/EdgeCasesRelationshipTest.php b/tests/Integration/ORM/EdgeCasesRelationshipTest.php index 6f35f1d3e..f1f5cc87d 100644 --- a/tests/Integration/ORM/EdgeCasesRelationshipTest.php +++ b/tests/Integration/ORM/EdgeCasesRelationshipTest.php @@ -13,12 +13,12 @@ namespace Zenstruck\Foundry\Tests\Integration\ORM; -use Zenstruck\Foundry\Tests\Fixture\Entity\EdgeCases\InversedOneToOneWithNonNullableOwning; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; use Zenstruck\Foundry\Persistence\PersistentObjectFactory; use Zenstruck\Foundry\Persistence\Proxy; use Zenstruck\Foundry\Test\Factories; use Zenstruck\Foundry\Test\ResetDatabase; +use Zenstruck\Foundry\Tests\Fixture\Entity\EdgeCases\InversedOneToOneWithNonNullableOwning; use Zenstruck\Foundry\Tests\Fixture\Entity\EdgeCases\RelationshipWithGlobalEntity; use Zenstruck\Foundry\Tests\Fixture\Entity\EdgeCases\RichDomainMandatoryRelationship; use Zenstruck\Foundry\Tests\Fixture\Entity\GlobalEntity; @@ -26,7 +26,6 @@ use Zenstruck\Foundry\Tests\Fixture\Stories\GlobalStory; use Zenstruck\Foundry\Tests\Integration\RequiresORM; -use function Zenstruck\Foundry\factory; use function Zenstruck\Foundry\Persistence\flush_after; use function Zenstruck\Foundry\Persistence\persistent_factory; use function Zenstruck\Foundry\Persistence\proxy_factory; From 0867ad6173343427046ba2275116bef5167b4f08 Mon Sep 17 00:00:00 2001 From: Nicolas PHILIPPE Date: Wed, 4 Dec 2024 14:25:28 +0100 Subject: [PATCH 024/102] feat: add `#[WithStory]` attribute (#728) --- docs/index.rst | 107 +++++++++++++----- phpunit | 4 +- src/Attribute/WithStory.php | 21 ++++ .../BootFoundryOnDataProviderMethodCalled.php | 2 +- src/PHPUnit/BuildStoryOnTestPrepared.php | 76 +++++++++++++ src/PHPUnit/FoundryExtension.php | 19 ++-- ...ownFoundryOnDataProviderMethodFinished.php | 2 +- tests/Fixture/Stories/ServiceStory.php | 36 ++++++ tests/Fixture/TestKernel.php | 2 + .../ParentClassWithStoryAttributeTestCase.php | 21 ++++ .../WithStory/WithStoryOnClassTest.php | 58 ++++++++++ .../WithStory/WithStoryOnMethodTest.php | 60 ++++++++++ .../WithStory/WithStoryOnParentClassTest.php | 30 +++++ 13 files changed, 394 insertions(+), 44 deletions(-) create mode 100644 src/Attribute/WithStory.php create mode 100644 src/PHPUnit/BuildStoryOnTestPrepared.php create mode 100644 tests/Fixture/Stories/ServiceStory.php create mode 100644 tests/Integration/Attribute/WithStory/ParentClassWithStoryAttributeTestCase.php create mode 100644 tests/Integration/Attribute/WithStory/WithStoryOnClassTest.php create mode 100644 tests/Integration/Attribute/WithStory/WithStoryOnMethodTest.php create mode 100644 tests/Integration/Attribute/WithStory/WithStoryOnParentClassTest.php diff --git a/docs/index.rst b/docs/index.rst index 26b7ec234..60612f845 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1608,35 +1608,26 @@ PHPUnit Data Providers It is possible to use factories in `PHPUnit data providers `_. -Their usage depends on which Foundry version you are running: +Their usage depends on whether you're using Foundry's `PHPUnit Extension`_ or not.: -Data Providers with Foundry ^2.2 -................................ +With PHPUnit Extension +...................... -From version 2.2, Foundry provides an extension for PHPUnit. -You can install it by modifying you ``phpunit.xml.dist``: +.. versionadded:: 2.2 -.. configuration-block:: + The ability to call ``Factory::create()`` in data providers was introduced in Foundry 2.2. - .. code-block:: xml - - - - - - - .. warning:: - This PHPUnit extension requires at least PHPUnit 11.4. + You will need at least PHPUnit 11.4 to call ``Factory::create()`` in your data providers. -Using this extension will allow to use your factories in your data providers the same way you're using them in tests. -Thanks to it, you can: +Thanks to Foundry's `PHPUnit Extension`_, you'll be able to use your factories in your data providers the same way +you're using them in tests. Thanks to it, you can: * Call ``->create()`` or ``::createOne()`` or any other method which creates objects in unit tests (using ``PHPUnit\Framework\TestCase``) and functional tests (``Symfony\Bundle\FrameworkBundle\Test\KernelTestCase``) * Use `Factories as Services`_ in functional tests * Use `faker()` normally, without wrapping its call in a callable - + :: use App\Factory\PostFactory; @@ -1654,21 +1645,22 @@ Thanks to it, you can: yield [PostWithServiceFactory::createOne()]; yield [PostFactory::createOne(['body' => faker()->sentence()]; } - - + .. warning:: Because Foundry is relying on its `Proxy mechanism `_, when using persistence, your factories must extend ``Zenstruck\Foundry\Persistence\PersistentProxyObjectFactory`` to work in your data providers. - + .. warning:: - For the same reason, you should not call methods from `Proxy` class in your data providers, - not even ``->_real()``. + For the same reason, you should not call methods from `Proxy` class in your data providers, not even ``->_real()``. + +Without PHPUnit Extension +......................... -Data Providers before Foundry v2.2 -.................................. +Data providers are computed early in the phpunit process before Foundry is booted. +Be sure your data provider returns only instances of ``Factory`` and you do not try to call ``->create()`` on them: :: @@ -1690,11 +1682,6 @@ Data Providers before Foundry v2.2 yield [PostFactory::new()->published()]; } -.. note:: - - Be sure your data provider returns only instances of ``Factory`` and you do not try to call ``->create()`` on them. - Data providers are computed early in the phpunit process before Foundry is booted. - .. note:: For the same reason as above, it is not possible to use `Factories as Services`_ with required @@ -2168,6 +2155,39 @@ Objects can be fetched from pools in your tests, fixtures or other stories: ProvinceStory::getRandomRange('be', 1, 4); // between 1 and 4 random Province|Proxy's from "be" pool ProvinceStory::getPool('be'); // all Province|Proxy's from "be" pool +#[WithStory] Attribute +~~~~~~~~~~~~~~~~~~~~~~ + +.. versionadded:: 2.3 + + The `#[WithStory]` attribute was added in Foundry 2.3. + +.. warning:: + + The `PHPUnit Extension`_ for Foundry is needed to use ``#[WithStory]`` attribute. + +You can use the ``#[WithStory]`` attribute to load stories in your tests: + +:: + + use App\Story\CategoryStory; + use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; + use Zenstruck\Foundry\Attribute\WithStory; + + // You can use the attribute on the class... + #[WithStory(CategoryStory::class)] + final class NeedsCategoriesTest extends KernelTestCase + { + // ... or on the method + #[WithStory(CategoryStory::class)] + public function testThatNeedStories(): void + { + // ... + } + } + +If used on the class, the story will be loaded before each test method. + Static Analysis --------------- @@ -2181,6 +2201,33 @@ Please, enable it with: $ vendor/bin/psalm-plugin enable zenstruck/foundry +PHPUnit Extension +----------------- + +Foundry is shipped with an extension for PHPUnit. You can install it by modifying the file ``phpunit.xml.dist``: + +.. configuration-block:: + + .. code-block:: xml + + + + + + + +This extension provides the following features: +- support for the `#[WithStory] Attribute`_ +- ability to use ``Factory::create()`` in `PHPUnit Data Providers`_ (along with PHPUnit ^11.4) + +.. versionadded:: 2.2 + + The PHPUnit extension was introduced in Foundry 2.2. + +.. warning:: + + The PHPUnit extension is only compatible with PHPUnit 10+. + Bundle Configuration -------------------- diff --git a/phpunit b/phpunit index c291e79ae..d14150c04 100755 --- a/phpunit +++ b/phpunit @@ -43,8 +43,8 @@ fi DAMA_EXTENSION="DAMA\DoctrineTestBundle\PHPUnit\PHPUnitExtension" FOUNDRY_EXTENSION="Zenstruck\Foundry\PHPUnit\FoundryExtension" -if [ "${USE_FOUNDRY_PHPUNIT_EXTENSION:-0}" = "1" ] && [ "${PHPUNIT_VERSION}" != "11" ]; then - echo "❌ USE_FOUNDRY_PHPUNIT_EXTENSION could only be used with PHPUNIT_VERSION=11"; +if [ "${USE_FOUNDRY_PHPUNIT_EXTENSION:-0}" = "1" ] && [ "${PHPUNIT_VERSION}" = "9" ]; then + echo "❌ USE_FOUNDRY_PHPUNIT_EXTENSION cannot be used with PHPUNIT_VERSION=10"; exit 1; fi diff --git a/src/Attribute/WithStory.php b/src/Attribute/WithStory.php new file mode 100644 index 000000000..8d2331b39 --- /dev/null +++ b/src/Attribute/WithStory.php @@ -0,0 +1,21 @@ + $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/PHPUnit/BootFoundryOnDataProviderMethodCalled.php b/src/PHPUnit/BootFoundryOnDataProviderMethodCalled.php index 2eccf412c..4564efb68 100644 --- a/src/PHPUnit/BootFoundryOnDataProviderMethodCalled.php +++ b/src/PHPUnit/BootFoundryOnDataProviderMethodCalled.php @@ -24,7 +24,7 @@ final class BootFoundryOnDataProviderMethodCalled implements Event\Test\DataProv public function notify(Event\Test\DataProviderMethodCalled $event): void { if (\method_exists($event->testMethod()->className(), '_bootForDataProvider')) { - \call_user_func([$event->testMethod()->className(), '_bootForDataProvider']); + $event->testMethod()->className()::_bootForDataProvider(); } } } diff --git a/src/PHPUnit/BuildStoryOnTestPrepared.php b/src/PHPUnit/BuildStoryOnTestPrepared.php new file mode 100644 index 000000000..67bf0e477 --- /dev/null +++ b/src/PHPUnit/BuildStoryOnTestPrepared.php @@ -0,0 +1,76 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\PHPUnit; + +use PHPUnit\Event; +use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; +use Zenstruck\Foundry\Attribute\WithStory; + +/** + * @internal + * @author Nicolas PHILIPPE + */ +final class BuildStoryOnTestPrepared implements Event\Test\PreparedSubscriber +{ + public function notify(Event\Test\Prepared $event): void + { + $test = $event->test(); + + if (!$test->isTestMethod()) { + return; + } + + /** @var Event\Code\TestMethod $test */ + + $reflectionClass = new \ReflectionClass($test->className()); + $withStoryAttributes = [ + ...$this->collectWithStoryAttributesFromClassAndParents($reflectionClass), + ...$reflectionClass->getMethod($test->methodName())->getAttributes(WithStory::class), + ]; + + if (!$withStoryAttributes) { + return; + } + + if (!is_subclass_of($test->className(), KernelTestCase::class)) { + throw new \InvalidArgumentException( + \sprintf( + 'The test class "%s" must extend "%s" to use the "%s" attribute.', + $test->className(), + KernelTestCase::class, + WithStory::class + ) + ); + } + + foreach ($withStoryAttributes as $withStoryAttribute) { + $withStoryAttribute->newInstance()->story::load(); + } + } + + /** + * @return list<\ReflectionAttribute> + */ + private function collectWithStoryAttributesFromClassAndParents(\ReflectionClass $class): array // @phpstan-ignore missingType.generics + { + return [ + ...$class->getAttributes(WithStory::class), + ...( + $class->getParentClass() + ? $this->collectWithStoryAttributesFromClassAndParents($class->getParentClass()) + : [] + ) + ]; + } +} diff --git a/src/PHPUnit/FoundryExtension.php b/src/PHPUnit/FoundryExtension.php index 738a1a16c..20cbb5a28 100644 --- a/src/PHPUnit/FoundryExtension.php +++ b/src/PHPUnit/FoundryExtension.php @@ -24,25 +24,24 @@ */ final class FoundryExtension implements Runner\Extension\Extension { - public const MIN_PHPUNIT_VERSION = '11.4'; - public function bootstrap( TextUI\Configuration\Configuration $configuration, Runner\Extension\Facade $facade, Runner\Extension\ParameterCollection $parameters, ): void { - if (!ConstraintRequirement::from(self::MIN_PHPUNIT_VERSION)->isSatisfiedBy(Runner\Version::id())) { - throw new \LogicException(\sprintf('Your PHPUnit version (%s) is not compatible with the minimum version (%s) needed to use this extension.', Runner\Version::id(), self::MIN_PHPUNIT_VERSION)); - } - // shutdown Foundry if for some reason it has been booted before if (Configuration::isBooted()) { Configuration::shutdown(); } - $facade->registerSubscribers( - new BootFoundryOnDataProviderMethodCalled(), - new ShutdownFoundryOnDataProviderMethodFinished(), - ); + $subscribers = [new BuildStoryOnTestPrepared()]; + + if (ConstraintRequirement::from('11.4')->isSatisfiedBy(Runner\Version::id())) { + // those deal with data provider events which can be useful only if PHPUnit 11.4 is used + $subscribers[] = new BootFoundryOnDataProviderMethodCalled(); + $subscribers[] = new ShutdownFoundryOnDataProviderMethodFinished(); + } + + $facade->registerSubscribers(...$subscribers); } } diff --git a/src/PHPUnit/ShutdownFoundryOnDataProviderMethodFinished.php b/src/PHPUnit/ShutdownFoundryOnDataProviderMethodFinished.php index 6fbe1394b..b028394b3 100644 --- a/src/PHPUnit/ShutdownFoundryOnDataProviderMethodFinished.php +++ b/src/PHPUnit/ShutdownFoundryOnDataProviderMethodFinished.php @@ -24,7 +24,7 @@ final class ShutdownFoundryOnDataProviderMethodFinished implements Event\Test\Da public function notify(Event\Test\DataProviderMethodFinished $event): void { if (\method_exists($event->testMethod()->className(), '_shutdownAfterDataProvider')) { - \call_user_func([$event->testMethod()->className(), '_shutdownAfterDataProvider']); + $event->testMethod()->className()::_shutdownAfterDataProvider(); } } } diff --git a/tests/Fixture/Stories/ServiceStory.php b/tests/Fixture/Stories/ServiceStory.php new file mode 100644 index 000000000..943652f8d --- /dev/null +++ b/tests/Fixture/Stories/ServiceStory.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\Stories; + +use Symfony\Component\HttpKernel\KernelInterface; +use Symfony\Component\Routing\RouterInterface; +use Zenstruck\Foundry\Story; +use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\GenericEntityFactory; + +/** + * @author Nicolas PHILIPPE + */ + final class ServiceStory extends Story +{ + public function __construct( + private readonly RouterInterface $router + ) { + } + + public function build(): void + { + $this->addState( + 'foo', + GenericEntityFactory::createOne(['prop1' => $this->router->getContext()->getHost()]) + ); + } +} diff --git a/tests/Fixture/TestKernel.php b/tests/Fixture/TestKernel.php index 63e618506..589e903e2 100644 --- a/tests/Fixture/TestKernel.php +++ b/tests/Fixture/TestKernel.php @@ -28,6 +28,7 @@ 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; /** @@ -162,6 +163,7 @@ protected function configureContainer(ContainerBuilder $c, LoaderInterface $load $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/ParentClassWithStoryAttributeTestCase.php b/tests/Integration/Attribute/WithStory/ParentClassWithStoryAttributeTestCase.php new file mode 100644 index 000000000..730f1e4f8 --- /dev/null +++ b/tests/Integration/Attribute/WithStory/ParentClassWithStoryAttributeTestCase.php @@ -0,0 +1,21 @@ + + */ +#[WithStory(EntityStory::class)] +abstract class ParentClassWithStoryAttributeTestCase extends KernelTestCase +{ + use Factories, ResetDatabase, RequiresORM; +} diff --git a/tests/Integration/Attribute/WithStory/WithStoryOnClassTest.php b/tests/Integration/Attribute/WithStory/WithStoryOnClassTest.php new file mode 100644 index 000000000..edb317c94 --- /dev/null +++ b/tests/Integration/Attribute/WithStory/WithStoryOnClassTest.php @@ -0,0 +1,58 @@ + + * @requires PHPUnit 11 + */ +#[RequiresPhpunit('11')] +#[RequiresPhpunitExtension(FoundryExtension::class)] +#[WithStory(EntityStory::class)] +final class WithStoryOnClassTest extends KernelTestCase +{ + use Factories, ResetDatabase, RequiresORM; + + /** + * @test + */ + public function can_use_story_in_attribute(): void + { + GenericEntityFactory::assert()->count(2); + + // ensure state is accessible + $this->assertSame('foo', EntityStory::get('foo')->getProp1()); + } + + /** + * @test + */ + #[WithStory(EntityStory::class)] + public function can_use_story_in_attribute_multiple_times(): void + { + GenericEntityFactory::assert()->count(2); + } + + /** + * @test + */ + #[WithStory(EntityPoolStory::class)] + public function can_use_another_story_at_level_class(): void + { + GenericEntityFactory::assert()->count(5); + } +} diff --git a/tests/Integration/Attribute/WithStory/WithStoryOnMethodTest.php b/tests/Integration/Attribute/WithStory/WithStoryOnMethodTest.php new file mode 100644 index 000000000..bc3b8193a --- /dev/null +++ b/tests/Integration/Attribute/WithStory/WithStoryOnMethodTest.php @@ -0,0 +1,60 @@ + + * @requires PHPUnit 11 + */ +#[RequiresPhpunit('11')] +#[RequiresPhpunitExtension(FoundryExtension::class)] +final class WithStoryOnMethodTest extends KernelTestCase +{ + use Factories, ResetDatabase, RequiresORM; + + /** + * @test + */ + #[WithStory(EntityStory::class)] + public function can_use_story_in_attribute(): void + { + GenericEntityFactory::assert()->count(2); + + // ensure state is accessible + $this->assertSame('foo', EntityStory::get('foo')->getProp1()); + } + + /** + * @test + */ + #[WithStory(EntityStory::class)] + #[WithStory(EntityPoolStory::class)] + public function can_use_multiple_story_in_attribute(): void + { + GenericEntityFactory::assert()->count(5); + } + + /** + * @test + */ + #[WithStory(ServiceStory::class)] + public function can_use_service_story(): void + { + $this->assertSame('localhost', ServiceStory::get('foo')->getProp1()); + } +} diff --git a/tests/Integration/Attribute/WithStory/WithStoryOnParentClassTest.php b/tests/Integration/Attribute/WithStory/WithStoryOnParentClassTest.php new file mode 100644 index 000000000..fdf337a6d --- /dev/null +++ b/tests/Integration/Attribute/WithStory/WithStoryOnParentClassTest.php @@ -0,0 +1,30 @@ + + * @requires PHPUnit 11 + */ +#[RequiresPhpunit('11')] +#[RequiresPhpunitExtension(FoundryExtension::class)] +#[WithStory(EntityPoolStory::class)] +final class WithStoryOnParentClassTest extends ParentClassWithStoryAttributeTestCase +{ + /** + * @test + */ + public function can_use_story_in_attribute_from_parent_class(): void + { + GenericEntityFactory::assert()->count(5); + } +} From d629c13e067f0ee9cd47cb52d02dcb55af6e043b Mon Sep 17 00:00:00 2001 From: nikophil Date: Wed, 4 Dec 2024 13:27:25 +0000 Subject: [PATCH 025/102] bot: fix cs [skip ci] --- src/PHPUnit/BuildStoryOnTestPrepared.php | 20 ++++++------------- .../ParentClassWithStoryAttributeTestCase.php | 11 +++++++++- .../WithStory/WithStoryOnClassTest.php | 11 +++++++++- .../WithStory/WithStoryOnMethodTest.php | 11 +++++++++- .../WithStory/WithStoryOnParentClassTest.php | 9 +++++++++ 5 files changed, 45 insertions(+), 17 deletions(-) diff --git a/src/PHPUnit/BuildStoryOnTestPrepared.php b/src/PHPUnit/BuildStoryOnTestPrepared.php index 67bf0e477..8689dbfee 100644 --- a/src/PHPUnit/BuildStoryOnTestPrepared.php +++ b/src/PHPUnit/BuildStoryOnTestPrepared.php @@ -32,7 +32,6 @@ public function notify(Event\Test\Prepared $event): void } /** @var Event\Code\TestMethod $test */ - $reflectionClass = new \ReflectionClass($test->className()); $withStoryAttributes = [ ...$this->collectWithStoryAttributesFromClassAndParents($reflectionClass), @@ -43,15 +42,8 @@ public function notify(Event\Test\Prepared $event): void return; } - if (!is_subclass_of($test->className(), KernelTestCase::class)) { - throw new \InvalidArgumentException( - \sprintf( - 'The test class "%s" must extend "%s" to use the "%s" attribute.', - $test->className(), - KernelTestCase::class, - WithStory::class - ) - ); + if (!\is_subclass_of($test->className(), KernelTestCase::class)) { + throw new \InvalidArgumentException(\sprintf('The test class "%s" must extend "%s" to use the "%s" attribute.', $test->className(), KernelTestCase::class, WithStory::class)); } foreach ($withStoryAttributes as $withStoryAttribute) { @@ -67,10 +59,10 @@ private function collectWithStoryAttributesFromClassAndParents(\ReflectionClass return [ ...$class->getAttributes(WithStory::class), ...( - $class->getParentClass() - ? $this->collectWithStoryAttributesFromClassAndParents($class->getParentClass()) - : [] - ) + $class->getParentClass() + ? $this->collectWithStoryAttributesFromClassAndParents($class->getParentClass()) + : [] + ), ]; } } diff --git a/tests/Integration/Attribute/WithStory/ParentClassWithStoryAttributeTestCase.php b/tests/Integration/Attribute/WithStory/ParentClassWithStoryAttributeTestCase.php index 730f1e4f8..0a3369277 100644 --- a/tests/Integration/Attribute/WithStory/ParentClassWithStoryAttributeTestCase.php +++ b/tests/Integration/Attribute/WithStory/ParentClassWithStoryAttributeTestCase.php @@ -2,6 +2,15 @@ declare(strict_types=1); +/* + * This file is part of the zenstruck/foundry package. + * + * (c) Kevin Bond + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Zenstruck\Foundry\Tests\Integration\Attribute\WithStory; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; @@ -17,5 +26,5 @@ #[WithStory(EntityStory::class)] abstract class ParentClassWithStoryAttributeTestCase extends KernelTestCase { - use Factories, ResetDatabase, RequiresORM; + use Factories, RequiresORM, ResetDatabase; } diff --git a/tests/Integration/Attribute/WithStory/WithStoryOnClassTest.php b/tests/Integration/Attribute/WithStory/WithStoryOnClassTest.php index edb317c94..08cccc00b 100644 --- a/tests/Integration/Attribute/WithStory/WithStoryOnClassTest.php +++ b/tests/Integration/Attribute/WithStory/WithStoryOnClassTest.php @@ -2,6 +2,15 @@ declare(strict_types=1); +/* + * This file is part of the zenstruck/foundry package. + * + * (c) Kevin Bond + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Zenstruck\Foundry\Tests\Integration\Attribute\WithStory; use PHPUnit\Framework\Attributes\RequiresPhpunit; @@ -25,7 +34,7 @@ #[WithStory(EntityStory::class)] final class WithStoryOnClassTest extends KernelTestCase { - use Factories, ResetDatabase, RequiresORM; + use Factories, RequiresORM, ResetDatabase; /** * @test diff --git a/tests/Integration/Attribute/WithStory/WithStoryOnMethodTest.php b/tests/Integration/Attribute/WithStory/WithStoryOnMethodTest.php index bc3b8193a..e98d028c0 100644 --- a/tests/Integration/Attribute/WithStory/WithStoryOnMethodTest.php +++ b/tests/Integration/Attribute/WithStory/WithStoryOnMethodTest.php @@ -2,6 +2,15 @@ declare(strict_types=1); +/* + * This file is part of the zenstruck/foundry package. + * + * (c) Kevin Bond + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Zenstruck\Foundry\Tests\Integration\Attribute\WithStory; use PHPUnit\Framework\Attributes\RequiresPhpunit; @@ -25,7 +34,7 @@ #[RequiresPhpunitExtension(FoundryExtension::class)] final class WithStoryOnMethodTest extends KernelTestCase { - use Factories, ResetDatabase, RequiresORM; + use Factories, RequiresORM, ResetDatabase; /** * @test diff --git a/tests/Integration/Attribute/WithStory/WithStoryOnParentClassTest.php b/tests/Integration/Attribute/WithStory/WithStoryOnParentClassTest.php index fdf337a6d..01c56f3f6 100644 --- a/tests/Integration/Attribute/WithStory/WithStoryOnParentClassTest.php +++ b/tests/Integration/Attribute/WithStory/WithStoryOnParentClassTest.php @@ -2,6 +2,15 @@ declare(strict_types=1); +/* + * This file is part of the zenstruck/foundry package. + * + * (c) Kevin Bond + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Zenstruck\Foundry\Tests\Integration\Attribute\WithStory; use PHPUnit\Framework\Attributes\RequiresPhpunit; From 403d9e923b96f693485b05f4caf04f3a66c76bc9 Mon Sep 17 00:00:00 2001 From: "Marien Fressinaud (pro account)" <107053378+marien-probesys@users.noreply.github.com> Date: Thu, 5 Dec 2024 11:50:15 +0100 Subject: [PATCH 026/102] fix: Fix the parameter name of the first and last methods (#730) --- docs/index.rst | 8 ++++---- src/Maker/Factory/MakeFactoryPHPDocMethod.php | 4 ++-- src/Persistence/ProxyRepositoryDecorator.php | 4 ++-- src/Persistence/RepositoryDecorator.php | 4 ++-- .../can_create_factory_for_entity_with_repository.php | 8 ++++---- ..._static_analysis_annotations_with_data_set_phpstan.php | 8 ++++---- ...th_static_analysis_annotations_with_data_set_psalm.php | 8 ++++---- 7 files changed, 22 insertions(+), 22 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index 60612f845..be7375087 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -218,8 +218,8 @@ This command will generate a ``PostFactory`` class that looks like this: * @method static Post|Proxy createOne(array $attributes = []) * @method static Post|Proxy find(object|array|mixed $criteria) * @method static Post|Proxy findOrCreate(array $attributes) - * @method static Post|Proxy first(string $sortedField = 'id') - * @method static Post|Proxy last(string $sortedField = 'id') + * @method static Post|Proxy first(string $sortBy = 'id') + * @method static Post|Proxy last(string $sortBy = 'id') * @method static Post|Proxy random(array $attributes = []) * @method static Post|Proxy randomOrCreate(array $attributes = [])) * @method static PostRepository|RepositoryProxy repository() @@ -234,8 +234,8 @@ This command will generate a ``PostFactory`` class that looks like this: * @phpstan-method static Proxy&Post createOne(array $attributes = []) * @phpstan-method static Proxy&Post find(object|array|mixed $criteria) * @phpstan-method static Proxy&Post findOrCreate(array $attributes) - * @phpstan-method static Proxy&Post first(string $sortedField = 'id') - * @phpstan-method static Proxy&Post last(string $sortedField = 'id') + * @phpstan-method static Proxy&Post first(string $sortBy = 'id') + * @phpstan-method static Proxy&Post last(string $sortBy = 'id') * @phpstan-method static Proxy&Post random(array $attributes = []) * @phpstan-method static Proxy&Post randomOrCreate(array $attributes = []) * @phpstan-method static list&Post> all() diff --git a/src/Maker/Factory/MakeFactoryPHPDocMethod.php b/src/Maker/Factory/MakeFactoryPHPDocMethod.php index b985e2ca2..22ceff09d 100644 --- a/src/Maker/Factory/MakeFactoryPHPDocMethod.php +++ b/src/Maker/Factory/MakeFactoryPHPDocMethod.php @@ -37,8 +37,8 @@ public static function createAll(MakeFactoryData $makeFactoryData): array if ($makeFactoryData->isPersisted()) { $methods[] = new self($makeFactoryData->getObjectShortName(), 'find(object|array|mixed $criteria)', returnsCollection: false); $methods[] = new self($makeFactoryData->getObjectShortName(), 'findOrCreate(array $attributes)', returnsCollection: false); - $methods[] = new self($makeFactoryData->getObjectShortName(), 'first(string $sortedField = \'id\')', returnsCollection: false); - $methods[] = new self($makeFactoryData->getObjectShortName(), 'last(string $sortedField = \'id\')', returnsCollection: false); + $methods[] = new self($makeFactoryData->getObjectShortName(), 'first(string $sortBy = \'id\')', returnsCollection: false); + $methods[] = new self($makeFactoryData->getObjectShortName(), 'last(string $sortBy = \'id\')', returnsCollection: false); $methods[] = new self($makeFactoryData->getObjectShortName(), 'random(array $attributes = [])', returnsCollection: false); $methods[] = new self($makeFactoryData->getObjectShortName(), 'randomOrCreate(array $attributes = [])', returnsCollection: false); diff --git a/src/Persistence/ProxyRepositoryDecorator.php b/src/Persistence/ProxyRepositoryDecorator.php index 666cb98af..21dfff8e9 100644 --- a/src/Persistence/ProxyRepositoryDecorator.php +++ b/src/Persistence/ProxyRepositoryDecorator.php @@ -44,9 +44,9 @@ public function firstOrFail(string $sortBy = 'id'): object * @return T|Proxy|null * @psalm-return (T&Proxy)|null */ - public function last(string $sortedField = 'id'): ?object + public function last(string $sortBy = 'id'): ?object { - return $this->proxyNullableObject(parent::last($sortedField)); + return $this->proxyNullableObject(parent::last($sortBy)); } /** diff --git a/src/Persistence/RepositoryDecorator.php b/src/Persistence/RepositoryDecorator.php index 3d5422c5b..bc8b925ad 100644 --- a/src/Persistence/RepositoryDecorator.php +++ b/src/Persistence/RepositoryDecorator.php @@ -73,9 +73,9 @@ public function firstOrFail(string $sortBy = 'id'): object /** * @return T|null */ - public function last(string $sortedField = 'id'): ?object + public function last(string $sortBy = 'id'): ?object { - return $this->findBy([], [$sortedField => 'DESC'], 1)[0] ?? null; + return $this->findBy([], [$sortBy => 'DESC'], 1)[0] ?? null; } /** diff --git a/tests/Fixture/Maker/expected/can_create_factory_for_entity_with_repository.php b/tests/Fixture/Maker/expected/can_create_factory_for_entity_with_repository.php index f92b522a5..f142ef0b0 100644 --- a/tests/Fixture/Maker/expected/can_create_factory_for_entity_with_repository.php +++ b/tests/Fixture/Maker/expected/can_create_factory_for_entity_with_repository.php @@ -25,8 +25,8 @@ * @method static GenericEntity|Proxy createOne(array $attributes = []) * @method static GenericEntity|Proxy find(object|array|mixed $criteria) * @method static GenericEntity|Proxy findOrCreate(array $attributes) - * @method static GenericEntity|Proxy first(string $sortedField = 'id') - * @method static GenericEntity|Proxy last(string $sortedField = 'id') + * @method static GenericEntity|Proxy first(string $sortBy = 'id') + * @method static GenericEntity|Proxy last(string $sortBy = 'id') * @method static GenericEntity|Proxy random(array $attributes = []) * @method static GenericEntity|Proxy randomOrCreate(array $attributes = []) * @method static GenericEntityRepository|ProxyRepositoryDecorator repository() @@ -41,8 +41,8 @@ * @phpstan-method static GenericEntity&Proxy createOne(array $attributes = []) * @phpstan-method static GenericEntity&Proxy find(object|array|mixed $criteria) * @phpstan-method static GenericEntity&Proxy findOrCreate(array $attributes) - * @phpstan-method static GenericEntity&Proxy first(string $sortedField = 'id') - * @phpstan-method static GenericEntity&Proxy last(string $sortedField = 'id') + * @phpstan-method static GenericEntity&Proxy first(string $sortBy = 'id') + * @phpstan-method static GenericEntity&Proxy last(string $sortBy = 'id') * @phpstan-method static GenericEntity&Proxy random(array $attributes = []) * @phpstan-method static GenericEntity&Proxy randomOrCreate(array $attributes = []) * @phpstan-method static ProxyRepositoryDecorator repository() diff --git a/tests/Fixture/Maker/expected/can_create_factory_with_static_analysis_annotations_with_data_set_phpstan.php b/tests/Fixture/Maker/expected/can_create_factory_with_static_analysis_annotations_with_data_set_phpstan.php index 679a6db58..e84320311 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 @@ -24,8 +24,8 @@ * @method static StandardCategory|Proxy createOne(array $attributes = []) * @method static StandardCategory|Proxy find(object|array|mixed $criteria) * @method static StandardCategory|Proxy findOrCreate(array $attributes) - * @method static StandardCategory|Proxy first(string $sortedField = 'id') - * @method static StandardCategory|Proxy last(string $sortedField = 'id') + * @method static StandardCategory|Proxy 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 static EntityRepository|ProxyRepositoryDecorator repository() @@ -40,8 +40,8 @@ * @phpstan-method static StandardCategory&Proxy createOne(array $attributes = []) * @phpstan-method static StandardCategory&Proxy find(object|array|mixed $criteria) * @phpstan-method static StandardCategory&Proxy findOrCreate(array $attributes) - * @phpstan-method static StandardCategory&Proxy first(string $sortedField = 'id') - * @phpstan-method static StandardCategory&Proxy last(string $sortedField = 'id') + * @phpstan-method static StandardCategory&Proxy 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() diff --git a/tests/Fixture/Maker/expected/can_create_factory_with_static_analysis_annotations_with_data_set_psalm.php b/tests/Fixture/Maker/expected/can_create_factory_with_static_analysis_annotations_with_data_set_psalm.php index 80b871e2e..670d80df3 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 @@ -24,8 +24,8 @@ * @method static StandardCategory|Proxy createOne(array $attributes = []) * @method static StandardCategory|Proxy find(object|array|mixed $criteria) * @method static StandardCategory|Proxy findOrCreate(array $attributes) - * @method static StandardCategory|Proxy first(string $sortedField = 'id') - * @method static StandardCategory|Proxy last(string $sortedField = 'id') + * @method static StandardCategory|Proxy 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 static EntityRepository|ProxyRepositoryDecorator repository() @@ -40,8 +40,8 @@ * @psalm-method static StandardCategory&Proxy createOne(array $attributes = []) * @psalm-method static StandardCategory&Proxy find(object|array|mixed $criteria) * @psalm-method static StandardCategory&Proxy findOrCreate(array $attributes) - * @psalm-method static StandardCategory&Proxy first(string $sortedField = 'id') - * @psalm-method static StandardCategory&Proxy last(string $sortedField = 'id') + * @psalm-method static StandardCategory&Proxy 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() From 4cb744703794581619e93a294c6cce7a24a7b10d Mon Sep 17 00:00:00 2001 From: Franck Ranaivo-Harisoa Date: Thu, 5 Dec 2024 18:50:07 +0100 Subject: [PATCH 027/102] Typo in Immutable section (#731) --- docs/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.rst b/docs/index.rst index be7375087..a06284751 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -680,7 +680,7 @@ instantiators): Immutable ~~~~~~~~~ -Factory's are immutable: +Factories are immutable: :: From c8f5046a492b2d24fa2fe6130a863c652a00aadc Mon Sep 17 00:00:00 2001 From: HypeMC <2445045+HypeMC@users.noreply.github.com> Date: Sun, 8 Dec 2024 08:05:45 +0100 Subject: [PATCH 028/102] Fix PHPUnit constraint requirement in FoundryExtension (#735) --- src/PHPUnit/FoundryExtension.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PHPUnit/FoundryExtension.php b/src/PHPUnit/FoundryExtension.php index 20cbb5a28..de88bb145 100644 --- a/src/PHPUnit/FoundryExtension.php +++ b/src/PHPUnit/FoundryExtension.php @@ -36,8 +36,8 @@ public function bootstrap( $subscribers = [new BuildStoryOnTestPrepared()]; - if (ConstraintRequirement::from('11.4')->isSatisfiedBy(Runner\Version::id())) { - // those deal with data provider events which can be useful only if PHPUnit 11.4 is used + if (ConstraintRequirement::from('>=11.4')->isSatisfiedBy(Runner\Version::id())) { + // those deal with data provider events which can be useful only if PHPUnit >=11.4 is used $subscribers[] = new BootFoundryOnDataProviderMethodCalled(); $subscribers[] = new ShutdownFoundryOnDataProviderMethodFinished(); } From 59867c3934c0eeee32059575488f5fedce419b4e Mon Sep 17 00:00:00 2001 From: Nicolas PHILIPPE Date: Sun, 8 Dec 2024 18:21:08 +0100 Subject: [PATCH 029/102] minor: change versions requirements (#737) --- .../Attribute/WithStory/WithStoryOnClassTest.php | 8 ++++++-- .../Attribute/WithStory/WithStoryOnMethodTest.php | 8 ++++++-- .../Attribute/WithStory/WithStoryOnParentClassTest.php | 6 ++++-- .../DataProviderForServiceFactoryInKernelTestCaseTest.php | 4 ++-- tests/Integration/DataProvider/DataProviderInUnitTest.php | 4 ++-- ...ataProviderWithNonProxyFactoryInKernelTestCaseTest.php | 4 ++-- .../DataProviderWithProxyFactoryInKernelTestCase.php | 4 ++-- .../DataProvider/GenericDocumentProxyFactoryTest.php | 4 ++-- .../DataProvider/GenericEntityProxyFactoryTest.php | 4 ++-- tests/Integration/Maker/MakeStoryTest.php | 1 - tests/Integration/Maker/MakerTestCase.php | 1 - 11 files changed, 28 insertions(+), 20 deletions(-) diff --git a/tests/Integration/Attribute/WithStory/WithStoryOnClassTest.php b/tests/Integration/Attribute/WithStory/WithStoryOnClassTest.php index 08cccc00b..44c94ab81 100644 --- a/tests/Integration/Attribute/WithStory/WithStoryOnClassTest.php +++ b/tests/Integration/Attribute/WithStory/WithStoryOnClassTest.php @@ -15,6 +15,7 @@ use PHPUnit\Framework\Attributes\RequiresPhpunit; use PHPUnit\Framework\Attributes\RequiresPhpunitExtension; +use PHPUnit\Framework\Attributes\Test; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; use Zenstruck\Foundry\Attribute\WithStory; use Zenstruck\Foundry\PHPUnit\FoundryExtension; @@ -27,9 +28,9 @@ /** * @author Nicolas PHILIPPE - * @requires PHPUnit 11 + * @requires PHPUnit ^11.0 */ -#[RequiresPhpunit('11')] +#[RequiresPhpunit('^11.0')] #[RequiresPhpunitExtension(FoundryExtension::class)] #[WithStory(EntityStory::class)] final class WithStoryOnClassTest extends KernelTestCase @@ -39,6 +40,7 @@ final class WithStoryOnClassTest extends KernelTestCase /** * @test */ + #[Test] public function can_use_story_in_attribute(): void { GenericEntityFactory::assert()->count(2); @@ -50,6 +52,7 @@ public function can_use_story_in_attribute(): void /** * @test */ + #[Test] #[WithStory(EntityStory::class)] public function can_use_story_in_attribute_multiple_times(): void { @@ -59,6 +62,7 @@ public function can_use_story_in_attribute_multiple_times(): void /** * @test */ + #[Test] #[WithStory(EntityPoolStory::class)] public function can_use_another_story_at_level_class(): void { diff --git a/tests/Integration/Attribute/WithStory/WithStoryOnMethodTest.php b/tests/Integration/Attribute/WithStory/WithStoryOnMethodTest.php index e98d028c0..2ae15d13e 100644 --- a/tests/Integration/Attribute/WithStory/WithStoryOnMethodTest.php +++ b/tests/Integration/Attribute/WithStory/WithStoryOnMethodTest.php @@ -15,6 +15,7 @@ use PHPUnit\Framework\Attributes\RequiresPhpunit; use PHPUnit\Framework\Attributes\RequiresPhpunitExtension; +use PHPUnit\Framework\Attributes\Test; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; use Zenstruck\Foundry\Attribute\WithStory; use Zenstruck\Foundry\PHPUnit\FoundryExtension; @@ -28,9 +29,9 @@ /** * @author Nicolas PHILIPPE - * @requires PHPUnit 11 + * @requires PHPUnit ^11.0 */ -#[RequiresPhpunit('11')] +#[RequiresPhpunit('^11.0')] #[RequiresPhpunitExtension(FoundryExtension::class)] final class WithStoryOnMethodTest extends KernelTestCase { @@ -39,6 +40,7 @@ final class WithStoryOnMethodTest extends KernelTestCase /** * @test */ + #[Test] #[WithStory(EntityStory::class)] public function can_use_story_in_attribute(): void { @@ -51,6 +53,7 @@ public function can_use_story_in_attribute(): void /** * @test */ + #[Test] #[WithStory(EntityStory::class)] #[WithStory(EntityPoolStory::class)] public function can_use_multiple_story_in_attribute(): void @@ -61,6 +64,7 @@ public function can_use_multiple_story_in_attribute(): void /** * @test */ + #[Test] #[WithStory(ServiceStory::class)] public function can_use_service_story(): void { diff --git a/tests/Integration/Attribute/WithStory/WithStoryOnParentClassTest.php b/tests/Integration/Attribute/WithStory/WithStoryOnParentClassTest.php index 01c56f3f6..59a9a1abb 100644 --- a/tests/Integration/Attribute/WithStory/WithStoryOnParentClassTest.php +++ b/tests/Integration/Attribute/WithStory/WithStoryOnParentClassTest.php @@ -15,6 +15,7 @@ use PHPUnit\Framework\Attributes\RequiresPhpunit; use PHPUnit\Framework\Attributes\RequiresPhpunitExtension; +use PHPUnit\Framework\Attributes\Test; use Zenstruck\Foundry\Attribute\WithStory; use Zenstruck\Foundry\PHPUnit\FoundryExtension; use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\GenericEntityFactory; @@ -22,9 +23,9 @@ /** * @author Nicolas PHILIPPE - * @requires PHPUnit 11 + * @requires PHPUnit ^11.0 */ -#[RequiresPhpunit('11')] +#[RequiresPhpunit('^11.0')] #[RequiresPhpunitExtension(FoundryExtension::class)] #[WithStory(EntityPoolStory::class)] final class WithStoryOnParentClassTest extends ParentClassWithStoryAttributeTestCase @@ -32,6 +33,7 @@ final class WithStoryOnParentClassTest extends ParentClassWithStoryAttributeTest /** * @test */ + #[Test] public function can_use_story_in_attribute_from_parent_class(): void { GenericEntityFactory::assert()->count(5); diff --git a/tests/Integration/DataProvider/DataProviderForServiceFactoryInKernelTestCaseTest.php b/tests/Integration/DataProvider/DataProviderForServiceFactoryInKernelTestCaseTest.php index 6a1df8e34..737b14375 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 6b0089c12..9f042eee9 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 50d9e2118..7b6544811 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 a502aad19..79da54529 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 { diff --git a/tests/Integration/DataProvider/GenericDocumentProxyFactoryTest.php b/tests/Integration/DataProvider/GenericDocumentProxyFactoryTest.php index ae69e50f2..c163c1df5 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 59d1488c6..d34064c86 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/Maker/MakeStoryTest.php b/tests/Integration/Maker/MakeStoryTest.php index 072bc932c..81f214088 100644 --- a/tests/Integration/Maker/MakeStoryTest.php +++ b/tests/Integration/Maker/MakeStoryTest.php @@ -17,7 +17,6 @@ /** * @author Kevin Bond * @group maker - * @requires PHP 8.1 */ final class MakeStoryTest extends MakerTestCase { diff --git a/tests/Integration/Maker/MakerTestCase.php b/tests/Integration/Maker/MakerTestCase.php index b2c974cf0..1e830d3d9 100644 --- a/tests/Integration/Maker/MakerTestCase.php +++ b/tests/Integration/Maker/MakerTestCase.php @@ -18,7 +18,6 @@ /** * @author Kevin Bond * @group maker - * @requires PHP 8.1 */ abstract class MakerTestCase extends KernelTestCase { From af64c35d3304f06beb7777e1e64ec3efa38709b5 Mon Sep 17 00:00:00 2001 From: Nicolas PHILIPPE Date: Tue, 10 Dec 2024 12:38:20 +0100 Subject: [PATCH 030/102] fix: detect if relation is oneToOne (#732) --- src/ORM/OrmV2PersistenceStrategy.php | 16 +++++++++--- src/ORM/OrmV3PersistenceStrategy.php | 21 ++++++++++----- src/Persistence/PersistentObjectFactory.php | 5 +++- src/Persistence/RelationshipMetadata.php | 1 + .../InverseSide.php | 1 + .../OwningSide.php | 1 + .../ManyToOneToSelfReferencing/OwningSide.php | 23 ++++++++++++++++ .../SelfReferencingInverseSide.php | 26 +++++++++++++++++++ .../ORM/EdgeCasesRelationshipTest.php | 15 +++++++++++ 9 files changed, 98 insertions(+), 11 deletions(-) create mode 100644 tests/Fixture/Entity/EdgeCases/ManyToOneToSelfReferencing/OwningSide.php create mode 100644 tests/Fixture/Entity/EdgeCases/ManyToOneToSelfReferencing/SelfReferencingInverseSide.php diff --git a/src/ORM/OrmV2PersistenceStrategy.php b/src/ORM/OrmV2PersistenceStrategy.php index 885b2b883..39bc366f3 100644 --- a/src/ORM/OrmV2PersistenceStrategy.php +++ b/src/ORM/OrmV2PersistenceStrategy.php @@ -29,17 +29,18 @@ public function relationshipMetadata(string $parent, string $child, string $fiel { $metadata = $this->classMetadata($parent); - $association = $this->getAssociationMapping($parent, $field); + $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: $association['type'] === ClassMetadataInfo::ONE_TO_ONE ); } - $inversedAssociation = $this->getAssociationMapping($child, $field); + $inversedAssociation = $this->getAssociationMapping($child, $parent, $field); if (null === $inversedAssociation || !$metadata instanceof ClassMetadataInfo) { return null; @@ -70,6 +71,7 @@ public function relationshipMetadata(string $parent, string $child, string $fiel isCascadePersist: $inversedAssociation['isCascadePersist'], inverseField: $metadata->isSingleValuedAssociation($association['fieldName']) ? $association['fieldName'] : null, isCollection: $inversedAssociationMetadata->isCollectionValuedAssociation($inversedAssociation['fieldName']), + isOneToOne: $inversedAssociation['type'] === ClassMetadataInfo::ONE_TO_ONE ); } @@ -78,12 +80,18 @@ public function relationshipMetadata(string $parent, string $child, string $fiel * @return array[]|null * @phpstan-return AssociationMapping|null */ - private function getAssociationMapping(string $entityClass, string $field): ?array + private function getAssociationMapping(string $entityClass, string $targetEntity, string $field): ?array { try { - return $this->objectManagerFor($entityClass)->getClassMetadata($entityClass)->getAssociationMapping($field); + $associationMapping = $this->objectManagerFor($entityClass)->getClassMetadata($entityClass)->getAssociationMapping($field); } catch (MappingException|ORMMappingException) { return null; } + + if (!is_a($targetEntity, $associationMapping['targetEntity'], allow_string: true)) { + return null; + } + + return $associationMapping; } } diff --git a/src/ORM/OrmV3PersistenceStrategy.php b/src/ORM/OrmV3PersistenceStrategy.php index 93de0b814..9ecae3ffa 100644 --- a/src/ORM/OrmV3PersistenceStrategy.php +++ b/src/ORM/OrmV3PersistenceStrategy.php @@ -17,6 +17,7 @@ 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; @@ -27,17 +28,18 @@ public function relationshipMetadata(string $parent, string $child, string $fiel { $metadata = $this->classMetadata($parent); - $association = $this->getAssociationMapping($parent, $field); + $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 + isCollection: $association instanceof ToManyAssociationMapping, + isOneToOne: $association instanceof OneToOneAssociationMapping, ); } - $inversedAssociation = $this->getAssociationMapping($child, $field); + $inversedAssociation = $this->getAssociationMapping($child, $parent, $field); if (null === $inversedAssociation || !$metadata instanceof ClassMetadata) { return null; @@ -60,19 +62,26 @@ public function relationshipMetadata(string $parent, string $child, string $fiel return new RelationshipMetadata( isCascadePersist: $inversedAssociation->isCascadePersist(), inverseField: $metadata->isSingleValuedAssociation($association->fieldName) ? $association->fieldName : null, - isCollection: $inversedAssociation instanceof ToManyAssociationMapping + isCollection: $inversedAssociation instanceof ToManyAssociationMapping, + isOneToOne: $inversedAssociation instanceof OneToOneAssociationMapping, ); } /** * @param class-string $entityClass */ - private function getAssociationMapping(string $entityClass, string $field): ?AssociationMapping + private function getAssociationMapping(string $entityClass, string $targetEntity, string $field): ?AssociationMapping { try { - return $this->objectManagerFor($entityClass)->getClassMetadata($entityClass)->getAssociationMapping($field); + $associationMapping = $this->objectManagerFor($entityClass)->getClassMetadata($entityClass)->getAssociationMapping($field); } catch (MappingException|ORMMappingException) { return null; } + + if (!is_a($targetEntity, $associationMapping->targetEntity, allow_string: true)) { + return null; + } + + return $associationMapping; } } diff --git a/src/Persistence/PersistentObjectFactory.php b/src/Persistence/PersistentObjectFactory.php index 8b3b9cac4..f2aabca44 100644 --- a/src/Persistence/PersistentObjectFactory.php +++ b/src/Persistence/PersistentObjectFactory.php @@ -272,7 +272,10 @@ protected function normalizeParameter(string $field, mixed $value): mixed $relationshipMetadata = $pm->relationshipMetadata($value::class(), static::class(), $field); // handle inversed OneToOne - if ($relationshipMetadata && !$relationshipMetadata->isCollection && $inverseField = $relationshipMetadata->inverseField) { + 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()); diff --git a/src/Persistence/RelationshipMetadata.php b/src/Persistence/RelationshipMetadata.php index fb9829ab4..fe1df93a9 100644 --- a/src/Persistence/RelationshipMetadata.php +++ b/src/Persistence/RelationshipMetadata.php @@ -22,6 +22,7 @@ public function __construct( public readonly bool $isCascadePersist, public readonly ?string $inverseField, public readonly bool $isCollection, + public readonly bool $isOneToOne, ) { } } diff --git a/tests/Fixture/Entity/EdgeCases/InversedOneToOneWithNonNullableOwning/InverseSide.php b/tests/Fixture/Entity/EdgeCases/InversedOneToOneWithNonNullableOwning/InverseSide.php index 82ab4e5a3..71537fa6c 100644 --- a/tests/Fixture/Entity/EdgeCases/InversedOneToOneWithNonNullableOwning/InverseSide.php +++ b/tests/Fixture/Entity/EdgeCases/InversedOneToOneWithNonNullableOwning/InverseSide.php @@ -19,6 +19,7 @@ * @author Nicolas PHILIPPE */ #[ORM\Entity] +#[ORM\Table('inversed_one_to_one_with_non_nullable_owning_inverse_side')] class InverseSide { #[ORM\Id] diff --git a/tests/Fixture/Entity/EdgeCases/InversedOneToOneWithNonNullableOwning/OwningSide.php b/tests/Fixture/Entity/EdgeCases/InversedOneToOneWithNonNullableOwning/OwningSide.php index 2c8c28cb6..006d09332 100644 --- a/tests/Fixture/Entity/EdgeCases/InversedOneToOneWithNonNullableOwning/OwningSide.php +++ b/tests/Fixture/Entity/EdgeCases/InversedOneToOneWithNonNullableOwning/OwningSide.php @@ -19,6 +19,7 @@ * @author Nicolas PHILIPPE */ #[ORM\Entity] +#[ORM\Table('inversed_one_to_one_with_non_nullable_owning_owning_side')] class OwningSide { #[ORM\Id] diff --git a/tests/Fixture/Entity/EdgeCases/ManyToOneToSelfReferencing/OwningSide.php b/tests/Fixture/Entity/EdgeCases/ManyToOneToSelfReferencing/OwningSide.php new file mode 100644 index 000000000..9e7327e8f --- /dev/null +++ b/tests/Fixture/Entity/EdgeCases/ManyToOneToSelfReferencing/OwningSide.php @@ -0,0 +1,23 @@ + + */ +#[ORM\Entity] +#[ORM\Table('many_to_one_to_self_referencing_owning_side')] +class OwningSide +{ + #[ORM\Id] + #[ORM\Column] + #[ORM\GeneratedValue(strategy: 'AUTO')] + public ?int $id = null; + + #[ORM\ManyToOne(inversedBy: 'owningSide')] + public ?SelfReferencingInverseSide $inverseSide = null; +} diff --git a/tests/Fixture/Entity/EdgeCases/ManyToOneToSelfReferencing/SelfReferencingInverseSide.php b/tests/Fixture/Entity/EdgeCases/ManyToOneToSelfReferencing/SelfReferencingInverseSide.php new file mode 100644 index 000000000..abc0dd5e6 --- /dev/null +++ b/tests/Fixture/Entity/EdgeCases/ManyToOneToSelfReferencing/SelfReferencingInverseSide.php @@ -0,0 +1,26 @@ + + */ +#[ORM\Entity] +#[ORM\Table('many_to_one_to_self_referencing_inverse_side')] +class SelfReferencingInverseSide +{ + #[ORM\Id] + #[ORM\Column] + #[ORM\GeneratedValue(strategy: 'AUTO')] + public ?int $id = null; + + #[ORM\ManyToOne()] + public ?SelfReferencingInverseSide $inverseSide = null; + + #[ORM\OneToOne(mappedBy: 'inverseSide')] + public ?OwningSide $owningSide = null; +} diff --git a/tests/Integration/ORM/EdgeCasesRelationshipTest.php b/tests/Integration/ORM/EdgeCasesRelationshipTest.php index f1f5cc87d..9476b3ee3 100644 --- a/tests/Integration/ORM/EdgeCasesRelationshipTest.php +++ b/tests/Integration/ORM/EdgeCasesRelationshipTest.php @@ -19,6 +19,7 @@ use Zenstruck\Foundry\Test\Factories; use Zenstruck\Foundry\Test\ResetDatabase; use Zenstruck\Foundry\Tests\Fixture\Entity\EdgeCases\InversedOneToOneWithNonNullableOwning; +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; @@ -123,6 +124,20 @@ public function inverse_one_to_one_with_non_nullable_inverse_side(): void self::assertSame($inverseSide, $inverseSide->owningSide->inverseSide); } + /** + * @test + */ + public function many_to_many_to_self_referencing_inverse_side(): void + { + $owningSideFactory = persistent_factory(ManyToOneToSelfReferencing\OwningSide::class); + $inverseSideFactory = persistent_factory(ManyToOneToSelfReferencing\SelfReferencingInverseSide::class); + + $owningSideFactory->create(['inverseSide' => $inverseSideFactory]); + + $owningSideFactory::assert()->count(1); + $inverseSideFactory::assert()->count(1); + } + /** * @test */ From f3d7341e31bf905aee81fe371308440956697280 Mon Sep 17 00:00:00 2001 From: nikophil Date: Tue, 10 Dec 2024 11:40:11 +0000 Subject: [PATCH 031/102] bot: fix cs [skip ci] --- src/ORM/OrmV2PersistenceStrategy.php | 6 +++--- src/ORM/OrmV3PersistenceStrategy.php | 2 +- .../EdgeCases/ManyToOneToSelfReferencing/OwningSide.php | 9 +++++++++ .../SelfReferencingInverseSide.php | 9 +++++++++ 4 files changed, 22 insertions(+), 4 deletions(-) diff --git a/src/ORM/OrmV2PersistenceStrategy.php b/src/ORM/OrmV2PersistenceStrategy.php index 39bc366f3..cf934db88 100644 --- a/src/ORM/OrmV2PersistenceStrategy.php +++ b/src/ORM/OrmV2PersistenceStrategy.php @@ -36,7 +36,7 @@ public function relationshipMetadata(string $parent, string $child, string $fiel isCascadePersist: $association['isCascadePersist'], inverseField: $metadata->isSingleValuedAssociation($association['fieldName']) ? $association['fieldName'] : null, isCollection: $metadata->isCollectionValuedAssociation($association['fieldName']), - isOneToOne: $association['type'] === ClassMetadataInfo::ONE_TO_ONE + isOneToOne: ClassMetadataInfo::ONE_TO_ONE === $association['type'] ); } @@ -71,7 +71,7 @@ public function relationshipMetadata(string $parent, string $child, string $fiel isCascadePersist: $inversedAssociation['isCascadePersist'], inverseField: $metadata->isSingleValuedAssociation($association['fieldName']) ? $association['fieldName'] : null, isCollection: $inversedAssociationMetadata->isCollectionValuedAssociation($inversedAssociation['fieldName']), - isOneToOne: $inversedAssociation['type'] === ClassMetadataInfo::ONE_TO_ONE + isOneToOne: ClassMetadataInfo::ONE_TO_ONE === $inversedAssociation['type'] ); } @@ -88,7 +88,7 @@ private function getAssociationMapping(string $entityClass, string $targetEntity return null; } - if (!is_a($targetEntity, $associationMapping['targetEntity'], allow_string: true)) { + if (!\is_a($targetEntity, $associationMapping['targetEntity'], allow_string: true)) { return null; } diff --git a/src/ORM/OrmV3PersistenceStrategy.php b/src/ORM/OrmV3PersistenceStrategy.php index 9ecae3ffa..95e240cbb 100644 --- a/src/ORM/OrmV3PersistenceStrategy.php +++ b/src/ORM/OrmV3PersistenceStrategy.php @@ -78,7 +78,7 @@ private function getAssociationMapping(string $entityClass, string $targetEntity return null; } - if (!is_a($targetEntity, $associationMapping->targetEntity, allow_string: true)) { + if (!\is_a($targetEntity, $associationMapping->targetEntity, allow_string: true)) { return null; } diff --git a/tests/Fixture/Entity/EdgeCases/ManyToOneToSelfReferencing/OwningSide.php b/tests/Fixture/Entity/EdgeCases/ManyToOneToSelfReferencing/OwningSide.php index 9e7327e8f..14f0c5728 100644 --- a/tests/Fixture/Entity/EdgeCases/ManyToOneToSelfReferencing/OwningSide.php +++ b/tests/Fixture/Entity/EdgeCases/ManyToOneToSelfReferencing/OwningSide.php @@ -2,6 +2,15 @@ declare(strict_types=1); +/* + * This file is part of the zenstruck/foundry package. + * + * (c) Kevin Bond + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Zenstruck\Foundry\Tests\Fixture\Entity\EdgeCases\ManyToOneToSelfReferencing; use Doctrine\ORM\Mapping as ORM; diff --git a/tests/Fixture/Entity/EdgeCases/ManyToOneToSelfReferencing/SelfReferencingInverseSide.php b/tests/Fixture/Entity/EdgeCases/ManyToOneToSelfReferencing/SelfReferencingInverseSide.php index abc0dd5e6..b1f9c9d77 100644 --- a/tests/Fixture/Entity/EdgeCases/ManyToOneToSelfReferencing/SelfReferencingInverseSide.php +++ b/tests/Fixture/Entity/EdgeCases/ManyToOneToSelfReferencing/SelfReferencingInverseSide.php @@ -2,6 +2,15 @@ declare(strict_types=1); +/* + * This file is part of the zenstruck/foundry package. + * + * (c) Kevin Bond + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Zenstruck\Foundry\Tests\Fixture\Entity\EdgeCases\ManyToOneToSelfReferencing; use Doctrine\ORM\Mapping as ORM; From dfe6bab854296a285cd8c1e0b72c5dec4fc36a14 Mon Sep 17 00:00:00 2001 From: Nicolas PHILIPPE Date: Tue, 10 Dec 2024 18:35:45 +0100 Subject: [PATCH 032/102] tests: add paratest permutation (#736) --- .github/workflows/ci.yml | 33 +++++++++++++++++++++++++++++++++ composer.json | 1 + phpunit | 2 +- phpunit-paratest.xml.dist | 30 ++++++++++++++++++++++++++++++ 4 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 phpunit-paratest.xml.dist diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f994b6213..ba6e86a02 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -92,6 +92,39 @@ jobs: run: ./phpunit shell: bash + test-with-paratest: + name: Test with paratest + runs-on: ubuntu-latest + env: + DATABASE_URL: 'mysql://root:root@localhost:3306/foundry?serverVersion=5.7.42' + MONGO_URL: '' + USE_DAMA_DOCTRINE_TEST_BUNDLE: 1 + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: 8.4 + coverage: none + tools: flex + + - name: Install dependencies + uses: ramsey/composer-install@v2 + with: + dependency-versions: highest + composer-options: --prefer-dist + env: + SYMFONY_REQUIRE: 7.2.* + + - name: Set up MySQL + run: sudo /etc/init.d/mysql start + + - name: Test + run: vendor/bin/paratest --processes 1 --configuration phpunit-paratest.xml.dist + shell: bash + code-coverage: name: Code Coverage runs-on: ubuntu-latest diff --git a/composer.json b/composer.json index aec2e99e4..8522d235f 100644 --- a/composer.json +++ b/composer.json @@ -27,6 +27,7 @@ }, "require-dev": { "bamarni/composer-bin-plugin": "^1.8", + "brianium/paratest": "^6|^7", "dama/doctrine-test-bundle": "^7.0|^8.0", "doctrine/collections": "^1.7|^2.0", "doctrine/common": "^3.2", diff --git a/phpunit b/phpunit index d14150c04..b62d8710b 100755 --- a/phpunit +++ b/phpunit @@ -35,7 +35,7 @@ SHOULD_UPDATE_PHPUNIT=$(check_phpunit_version "${PHPUNIT_VERSION}") if [ "${SHOULD_UPDATE_PHPUNIT}" = "0" ]; then echo "ℹ️ Upgrading PHPUnit to ${PHPUNIT_VERSION}" - composer update "phpunit/phpunit:^${PHPUNIT_VERSION}" -W + composer update brianium/paratest "phpunit/phpunit:^${PHPUNIT_VERSION}" -W fi ### << diff --git a/phpunit-paratest.xml.dist b/phpunit-paratest.xml.dist new file mode 100644 index 000000000..189f603c3 --- /dev/null +++ b/phpunit-paratest.xml.dist @@ -0,0 +1,30 @@ + + + + + + + + + + + + tests + + + + + src + + + + + + + From 854220f3f2bacabf277db0cb0aa70c55c0660d15 Mon Sep 17 00:00:00 2001 From: Oskar Stark Date: Wed, 11 Dec 2024 10:25:28 +0100 Subject: [PATCH 033/102] Figo highlighting and use CPP --- docs/index.rst | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index a06284751..2bcbaf5c4 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -2025,24 +2025,19 @@ If your stories require dependencies, you can define them as a service: namespace App\Story; use App\Factory\PostFactory; - use App\Service\ServiceA; - use App\Service\ServiceB; + use App\Service\MyService; use Zenstruck\Foundry\Story; final class PostStory extends Story { - private $serviceA; - private $serviceB; - - public function __construct(ServiceA $serviceA, ServiceB $serviceB) - { - $this->serviceA = $serviceA; - $this->serviceB = $serviceB; + public function __construct( + private MyService $myService, + ) { } public function build(): void { - // can use $this->serviceA, $this->serviceB here to help build this story + // $this->myService can be used here here to help build this story } } @@ -2160,7 +2155,7 @@ Objects can be fetched from pools in your tests, fixtures or other stories: .. versionadded:: 2.3 - The `#[WithStory]` attribute was added in Foundry 2.3. + The ``#[WithStory]`` attribute was added in Foundry 2.3. .. warning:: From b16b227e7e75cd642b72218fa7908d470c4f1a52 Mon Sep 17 00:00:00 2001 From: Oskar Stark Date: Wed, 11 Dec 2024 11:30:35 +0100 Subject: [PATCH 034/102] Update index.rst Co-authored-by: Nicolas PHILIPPE --- docs/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.rst b/docs/index.rst index 2bcbaf5c4..9a38cdff0 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -2037,7 +2037,7 @@ If your stories require dependencies, you can define them as a service: public function build(): void { - // $this->myService can be used here here to help build this story + // $this->myService can be used here to help build this story } } From cbdb4de02874505cc2c99669ee97f93c3cb8e028 Mon Sep 17 00:00:00 2001 From: Nicolas PHILIPPE Date: Wed, 11 Dec 2024 16:46:23 +0100 Subject: [PATCH 035/102] changelog: update [skip ci] --- CHANGELOG.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ec839323..811863870 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,25 @@ # CHANGELOG +## [v2.3.0](https://github.com/zenstruck/foundry/releases/tag/v2.3.0) + +December 11th, 2024 - [v2.2.2...v2.3.0](https://github.com/zenstruck/foundry/compare/v2.2.2...v2.3.0) + +* b16b227 Update index.rst (#740) by @OskarStark, @nikophil +* 854220f Figo highlighting and use CPP (#740) by @OskarStark +* dfe6bab tests: add paratest permutation (#736) by @nikophil +* af64c35 fix: detect if relation is oneToOne (#732) by @nikophil +* 59867c3 minor: change versions requirements (#737) by @nikophil +* c8f5046 Fix PHPUnit constraint requirement in FoundryExtension (#735) by @HypeMC +* 4cb7447 Typo in Immutable section (#731) by @franckranaivo +* 403d9e9 fix: Fix the parameter name of the first and last methods (#730) by @marien-probesys +* 0867ad6 feat: add `#[WithStory]` attribute (#728) by @nikophil +* c5d0bdd fix: can create inversed one to one with non nullable (#726) by @nikophil +* 0e7ac6f docs: Fix Story phpdocs (#727) by @simondaigre, @nikophil +* f48ffd1 fix: can create inversed one to one (#659) by @nikophil +* 6d08784 fix: bug with one to many (#722) by @nikophil +* efadea8 docs:fix code blocks not showing up (#723) by @AndreasA +* edf287e minor: Add templated types to flush_after (#719) by @BackEndTea + ## [v2.2.2](https://github.com/zenstruck/foundry/releases/tag/v2.2.2) November 5th, 2024 - [v2.2.1...v2.2.2](https://github.com/zenstruck/foundry/compare/v2.2.1...v2.2.2) From cd9dbf54c728e42e6a557760bf2ac1292b99f827 Mon Sep 17 00:00:00 2001 From: Nicolas PHILIPPE Date: Sun, 1 Sep 2024 18:01:01 +0200 Subject: [PATCH 036/102] refactor: extract reset:migration tests in another testsuite --- .env | 10 +- .github/workflows/ci.yml | 68 +++++++++- .gitignore | 2 +- README.md | 30 +++-- composer.json | 26 ++-- phpunit-10.xml.dist | 9 +- phpunit-paratest.xml.dist | 3 +- phpunit.xml.dist | 7 +- src/Mongo/MongoSchemaResetter.php | 12 +- src/ORM/ResetDatabase/BaseOrmResetter.php | 51 +++++--- .../ResetDatabase/MigrateDatabaseResetter.php | 16 ++- .../ResetDatabase/SchemaDatabaseResetter.php | 23 ++-- src/Persistence/SymfonyCommandRunner.php | 49 -------- src/symfony_console.php | 40 ++++++ .../Version20240611065130.php | 44 ------- .../Entity/Contact/StandardContact.php | 2 +- .../SelfReferencingInverseSide.php | 2 +- .../EntityInAnotherSchema/Article.php | 8 +- .../MigrationTests/TestMigrationKernel.php | 119 ++++++++++++++++++ .../migration-configuration-transactional.php | 8 ++ .../configs/migration-configuration.php | 8 ++ tests/Fixture/TestKernel.php | 41 +++--- .../ResetDatabaseWithMigrationTest.php | 97 ++++++++++++++ tests/bootstrap-migrate.php | 48 +++++++ tests/bootstrap.php | 31 ----- 25 files changed, 520 insertions(+), 234 deletions(-) delete mode 100644 src/Persistence/SymfonyCommandRunner.php create mode 100644 src/symfony_console.php delete mode 100644 tests/Fixture/CustomMigrations/Version20240611065130.php rename tests/Fixture/{EdgeCases/Migrate/ORM => }/EntityInAnotherSchema/Article.php (68%) create mode 100644 tests/Fixture/MigrationTests/TestMigrationKernel.php create mode 100644 tests/Fixture/MigrationTests/configs/migration-configuration-transactional.php create mode 100644 tests/Fixture/MigrationTests/configs/migration-configuration.php create mode 100644 tests/Integration/Migration/ResetDatabaseWithMigrationTest.php create mode 100644 tests/bootstrap-migrate.php diff --git a/.env b/.env index d443158a7..673ee0958 100644 --- a/.env +++ b/.env @@ -1,6 +1,12 @@ +# DBMS can be changed by adding the following in .env.local: +#DATABASE_URL="postgresql://zenstruck:zenstruck@127.0.0.1:5433/zenstruck_foundry?serverVersion=15" +#DATABASE_URL="sqlite:///%kernel.project_dir%/var/data.db" DATABASE_URL="mysql://root:1234@127.0.0.1:3307/foundry_test?serverVersion=5.7.42" + +# Mongo ca be disabled with the following in .env.local: +# MONGO_URL="" MONGO_URL="mongodb://127.0.0.1:27018/dbName?compressors=disabled&gssapiServiceName=mongodb" -DATABASE_RESET_MODE="schema" + USE_DAMA_DOCTRINE_TEST_BUNDLE="0" USE_FOUNDRY_PHPUNIT_EXTENSION="0" -PHPUNIT_VERSION="9" +PHPUNIT_VERSION="9" # allowed values: 9, 10, 11 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ba6e86a02..936e3c130 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,7 +8,7 @@ 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)' || '' }}${{ !contains(matrix.database, 'sql') && '' || matrix.use-migrate == 1 && ' (migrate)' || ' (schema)' }}${{ matrix.use-phpunit-extension == 1 && ' (phpunit extension)' || '' }} + 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)' || '' }} runs-on: ubuntu-latest strategy: fail-fast: false @@ -20,7 +20,6 @@ jobs: # default values: # deps: [ highest ] # without-dama: [ 0 ] - # use-migrate: [ 0 ] # use-phpunit-extension: [ 0 ] # phpunit: [ 9 ] @@ -35,7 +34,6 @@ jobs: - {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, use-migrate: 1} - {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} @@ -46,7 +44,6 @@ jobs: USE_DAMA_DOCTRINE_TEST_BUNDLE: ${{ matrix.without-dama == 0 && contains(matrix.database, 'sql') && 1 || 0 }} USE_FOUNDRY_PHPUNIT_EXTENSION: ${{ matrix.use-phpunit-extension || 0 }} PHPUNIT_VERSION: ${{ matrix.phpunit || 9 }} - DATABASE_RESET_MODE: ${{ matrix.use-migrate == 1 && 'migrate' || 'schema' }} services: postgres: image: ${{ contains(matrix.database, 'pgsql') && 'postgres:15' || '' }} @@ -92,6 +89,69 @@ 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)' || '' }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + database: [ mysql, pgsql, sqlite ] + 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' + 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: '' + USE_DAMA_DOCTRINE_TEST_BUNDLE: ${{ matrix.use-dama == 1 && 1 || 0 }} + WITH_MIGRATION_CONFIGURATION_FILE: ${{ matrix.with-migration-configuration-file }} + PHPUNIT_VERSION: 9 + services: + postgres: + image: ${{ contains(matrix.database, 'pgsql') && 'postgres:15' || '' }} + env: + POSTGRES_USER: root + POSTGRES_PASSWORD: root + POSTGRES_DB: foundry + ports: + - 5432:5432 + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: 8.3 + coverage: none + tools: flex + + - name: Install dependencies + uses: ramsey/composer-install@v2 + with: + dependency-versions: highest + composer-options: --prefer-dist + env: + SYMFONY_REQUIRE: 7.1.* + + - name: Set up MySQL + if: contains(matrix.database, 'mysql') + run: sudo /etc/init.d/mysql start + + - name: Test + run: ./phpunit --testsuite migrate --bootstrap tests/bootstrap-migrate.php + shell: bash + test-with-paratest: name: Test with paratest runs-on: ubuntu-latest diff --git a/.gitignore b/.gitignore index ee9e61fd3..6d5aba94f 100644 --- a/.gitignore +++ b/.gitignore @@ -14,5 +14,5 @@ /docker/.makefile /.env.local /docker-compose.override.yaml -/tests/Fixture/Migrations/ +/tests/Fixture/MigrationTests/Migrations/ /tests/Fixture/Maker/tmp/ diff --git a/README.md b/README.md index 4472e2796..ba954bc3d 100644 --- a/README.md +++ b/README.md @@ -47,20 +47,15 @@ $ docker compose up --detach # install dependencies $ composer update -# run test suite with all available permutations -$ composer test - -# run only one permutation +# run main testsuite (with "schema" reset database strategy) +$ composer test-schema +# or $ ./phpunit -# run test suite with dama/doctrine-test-bundle -$ USE_DAMA_DOCTRINE_TEST_BUNDLE=1 vendor/bin/phpunit - -# run test suite with postgreSQL instead of MySQL -$ DATABASE_URL="postgresql://zenstruck:zenstruck@127.0.0.1:5433/zenstruck_foundry?serverVersion=15" vendor/bin/phpunit - -# run test suite with another PHPUnit version -$ PHPUNIT_VERSION=10 vendor/bin/phpunit +# run "migrate" testsuite (with "migrate" reset database strategy) +$ composer test-migrate +# or +$ ./phpunit --testsuite migrate --bootstrap tests/bootstrap-migrate.php ``` ### Overriding the default configuration @@ -69,11 +64,20 @@ You can override default environment variables by creating a `.env.local` file, ```bash # .env.local -DATABASE_URL="postgresql://zenstruck:zenstruck@127.0.0.1:5433/zenstruck_foundry?serverVersion=15" # enables postgreSQL instead of MySQL + +# change the database to postgreSQL... +DATABASE_URL="postgresql://zenstruck:zenstruck@127.0.0.1:5433/zenstruck_foundry?serverVersion=15" +# ...or to SQLite +DATABASE_URL="sqlite:///%kernel.project_dir%/var/data.db" + MONGO_URL="" # disables Mongo USE_DAMA_DOCTRINE_TEST_BUNDLE="1" # enables dama/doctrine-test-bundle PHPUNIT_VERSION="11" # possible values: 9, 10, 11, 11.4 +# test reset database with configuration migration, +# only relevant for "migrate" testsuite +WITH_MIGRATION_CONFIGURATION_FILE="tests/Fixture/MigrationTests/configs/migration-configuration.php" + # run test suite with postgreSQL $ vendor/bin/phpunit ``` diff --git a/composer.json b/composer.json index 8522d235f..93efd2223 100644 --- a/composer.json +++ b/composer.json @@ -49,7 +49,12 @@ "Zenstruck\\Foundry\\": "src/", "Zenstruck\\Foundry\\Psalm\\": "utils/psalm" }, - "files": ["src/functions.php", "src/Persistence/functions.php", "src/phpunit_helper.php"] + "files": [ + "src/functions.php", + "src/Persistence/functions.php", + "src/phpunit_helper.php", + "src/symfony_console.php" + ] }, "autoload-dev": { "psr-4": { @@ -78,22 +83,15 @@ }, "scripts": { "test": [ - "@test-schema-no-dama", - "@test-migrate-no-dama", - "@test-schema-dama", - "@test-migrate-dama" + "@test-schema", + "@test-migrate" ], - "test-schema-no-dama": "DATABASE_RESET_MODE=schema USE_DAMA_DOCTRINE_TEST_BUNDLE=0 ./phpunit", - "test-migrate-no-dama": "DATABASE_RESET_MODE=migrate USE_DAMA_DOCTRINE_TEST_BUNDLE=0 ./phpunit", - "test-schema-dama": "DATABASE_RESET_MODE=schema USE_DAMA_DOCTRINE_TEST_BUNDLE=1 ./phpunit", - "test-migrate-dama": "DATABASE_RESET_MODE=migrate USE_DAMA_DOCTRINE_TEST_BUNDLE=1 ./phpunit" + "test-schema": "./phpunit", + "test-migrate": "./phpunit --testsuite migrate --bootstrap tests/bootstrap-migrate.php" }, "scripts-descriptions": { - "test": "Run all test permutations", - "test-schema-no-dama": "Test with schema reset (no dama/doctrine-test-bundle)", - "test-migrate-no-dama": "Test with migrations reset (no dama/doctrine-test-bundle)", - "test-schema-dama": "Test with schema reset and dama/doctrine-test-bundle", - "test-migrate-dama": "Test with migrations reset and dama/doctrine-test-bundle" + "test-schema": "Test with schema reset", + "test-migrate": "Test with migrations reset" }, "minimum-stability": "dev", "prefer-stable": true diff --git a/phpunit-10.xml.dist b/phpunit-10.xml.dist index 21bfa9c48..b97581549 100644 --- a/phpunit-10.xml.dist +++ b/phpunit-10.xml.dist @@ -6,7 +6,8 @@ colors="true" failOnRisky="true" failOnWarning="true" - cacheDirectory=".phpunit.cache"> + cacheDirectory=".phpunit.cache" + defaultTestSuite="main"> @@ -14,8 +15,12 @@ - + tests + tests/Integration/Migration/ResetDatabaseWithMigrationTest.php + + + tests/Integration/Migration/ResetDatabaseWithMigrationTest.php diff --git a/phpunit-paratest.xml.dist b/phpunit-paratest.xml.dist index 189f603c3..66b9f9a74 100644 --- a/phpunit-paratest.xml.dist +++ b/phpunit-paratest.xml.dist @@ -14,8 +14,9 @@ - + tests + tests/Integration/Migration/ResetDatabaseWithMigrationTest.php diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 4c12dd39b..c6b9f26cb 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -7,6 +7,7 @@ colors="true" failOnRisky="true" failOnWarning="true" + defaultTestSuite="main" > @@ -16,8 +17,12 @@ - + tests + tests/Integration/Migration/ResetDatabaseWithMigrationTest.php + + + tests/Integration/Migration/ResetDatabaseWithMigrationTest.php diff --git a/src/Mongo/MongoSchemaResetter.php b/src/Mongo/MongoSchemaResetter.php index 555eb5535..ad40159fc 100644 --- a/src/Mongo/MongoSchemaResetter.php +++ b/src/Mongo/MongoSchemaResetter.php @@ -14,7 +14,9 @@ namespace Zenstruck\Foundry\Mongo; use Symfony\Component\HttpKernel\KernelInterface; -use Zenstruck\Foundry\Persistence\SymfonyCommandRunner; + +use function Zenstruck\Foundry\application; +use function Zenstruck\Foundry\runCommand; /** * @internal @@ -22,8 +24,6 @@ */ final class MongoSchemaResetter implements MongoResetter { - use SymfonyCommandRunner; - /** * @param list $managers */ @@ -33,15 +33,15 @@ public function __construct(private array $managers) public function resetBeforeEachTest(KernelInterface $kernel): void { - $application = self::application($kernel); + $application = application($kernel); foreach ($this->managers as $manager) { try { - self::runCommand($application, 'doctrine:mongodb:schema:drop', ['--dm' => $manager]); + runCommand($application, "doctrine:mongodb:schema:drop --dm={$manager}"); } catch (\Exception) { } - self::runCommand($application, 'doctrine:mongodb:schema:create', ['--dm' => $manager]); + runCommand($application, "doctrine:mongodb:schema:create --dm={$manager}"); } } } diff --git a/src/ORM/ResetDatabase/BaseOrmResetter.php b/src/ORM/ResetDatabase/BaseOrmResetter.php index 4044fe281..8f2bf840e 100644 --- a/src/ORM/ResetDatabase/BaseOrmResetter.php +++ b/src/ORM/ResetDatabase/BaseOrmResetter.php @@ -9,15 +9,20 @@ use Doctrine\DBAL\Platforms\PostgreSQLPlatform; use Doctrine\DBAL\Platforms\SQLitePlatform; use Symfony\Bundle\FrameworkBundle\Console\Application; -use Zenstruck\Foundry\Persistence\SymfonyCommandRunner; +use Symfony\Component\Filesystem\Filesystem; +use Symfony\Component\HttpKernel\KernelInterface; + +use Zenstruck\Foundry\ORM\DoctrineOrmVersionGuesser; + +use function Zenstruck\Foundry\runCommand; /** * @author Nicolas PHILIPPE * @internal */ -abstract class BaseOrmResetter +abstract class BaseOrmResetter implements OrmResetter { - use SymfonyCommandRunner; + private static bool $inFirstTest = true; /** * @param list $managers @@ -30,6 +35,19 @@ public function __construct( ) { } + final public function resetBeforeEachTest(KernelInterface $kernel): void + { + if (self::$inFirstTest) { + self::$inFirstTest = false; + + return; + } + + $this->doResetBeforeEachTest($kernel); + } + + abstract protected function doResetBeforeEachTest(KernelInterface $kernel): void; + final protected function dropAndResetDatabase(Application $application): void { foreach ($this->connections as $connectionName) { @@ -39,29 +57,26 @@ final protected function dropAndResetDatabase(Application $application): void if ($databasePlatform instanceof SQLitePlatform) { // we don't need to create the sqlite database - it's created when the schema is created + // let's only drop the .db file + + $dbPath = $connection->getParams()['path'] ?? null; + $fs = new Filesystem(); + if (DoctrineOrmVersionGuesser::isOrmV3() && $dbPath && $fs->exists($dbPath)) { + (new Filesystem())->remove($dbPath); + } + continue; } if ($databasePlatform instanceof PostgreSQLPlatform) { // let's drop all connections to the database to be able to drop it - self::runCommand( - $application, - 'dbal:run-sql', - [ - '--connection' => $connectionName, - 'sql' => 'SELECT pid, pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = current_database() AND pid <> pg_backend_pid()', - ], - canFail: true, - ); + $sql = 'SELECT pid, pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = current_database() AND pid <> pg_backend_pid()'; + runCommand($application, "dbal:run-sql --connection={$connectionName} '{$sql}'", canFail: true); } - self::runCommand( - $application, - 'doctrine:database:drop', - ['--connection' => $connectionName, '--force' => true, '--if-exists' => true] - ); + runCommand($application, "doctrine:database:drop --connection={$connectionName} --force --if-exists"); - self::runCommand($application, 'doctrine:database:create', ['--connection' => $connectionName]); + runCommand($application, "doctrine:database:create --connection={$connectionName}"); } } } diff --git a/src/ORM/ResetDatabase/MigrateDatabaseResetter.php b/src/ORM/ResetDatabase/MigrateDatabaseResetter.php index fd99b57b2..52a156980 100644 --- a/src/ORM/ResetDatabase/MigrateDatabaseResetter.php +++ b/src/ORM/ResetDatabase/MigrateDatabaseResetter.php @@ -5,13 +5,17 @@ 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; +use function Zenstruck\Foundry\runCommand; + /** * @internal * @author Nicolas PHILIPPE */ -final class MigrateDatabaseResetter extends BaseOrmResetter implements OrmResetter +final class MigrateDatabaseResetter extends BaseOrmResetter { /** * @param list $configurations @@ -26,30 +30,30 @@ public function __construct( parent::__construct($registry, $managers, $connections); } - final public function resetBeforeEachTest(KernelInterface $kernel): void + final public function resetBeforeFirstTest(KernelInterface $kernel): void { $this->resetWithMigration($kernel); } - public function resetBeforeFirstTest(KernelInterface $kernel): void + public function doResetBeforeEachTest(KernelInterface $kernel): void { $this->resetWithMigration($kernel); } private function resetWithMigration(KernelInterface $kernel): void { - $application = self::application($kernel); + $application = application($kernel); $this->dropAndResetDatabase($application); if (!$this->configurations) { - self::runCommand($application, 'doctrine:migrations:migrate'); + runCommand($application, 'doctrine:migrations:migrate'); return; } foreach ($this->configurations as $configuration) { - self::runCommand($application, 'doctrine:migrations:migrate', ['--configuration' => $configuration]); + runCommand($application, "doctrine:migrations:migrate --configuration={$configuration}"); } } } diff --git a/src/ORM/ResetDatabase/SchemaDatabaseResetter.php b/src/ORM/ResetDatabase/SchemaDatabaseResetter.php index 40dc765eb..12342b430 100644 --- a/src/ORM/ResetDatabase/SchemaDatabaseResetter.php +++ b/src/ORM/ResetDatabase/SchemaDatabaseResetter.php @@ -16,23 +16,26 @@ use Symfony\Bundle\FrameworkBundle\Console\Application; use Symfony\Component\HttpKernel\KernelInterface; +use function Zenstruck\Foundry\application; +use function Zenstruck\Foundry\runCommand; + /** * @internal * @author Nicolas PHILIPPE */ -final class SchemaDatabaseResetter extends BaseOrmResetter implements OrmResetter +final class SchemaDatabaseResetter extends BaseOrmResetter { public function resetBeforeFirstTest(KernelInterface $kernel): void { - $application = self::application($kernel); + $application = application($kernel); $this->dropAndResetDatabase($application); $this->createSchema($application); } - public function resetBeforeEachTest(KernelInterface $kernel): void + public function doResetBeforeEachTest(KernelInterface $kernel): void { - $application = self::application($kernel); + $application = application($kernel); $this->dropSchema($application); $this->createSchema($application); @@ -41,22 +44,14 @@ public function resetBeforeEachTest(KernelInterface $kernel): void private function createSchema(Application $application): void { foreach ($this->managers as $manager) { - self::runCommand( - $application, - 'doctrine:schema:update', - ['--em' => $manager, '--force' => true] - ); + runCommand($application, "doctrine:schema:update --em={$manager} --force -v"); } } private function dropSchema(Application $application): void { foreach ($this->managers as $manager) { - self::runCommand( - $application, - 'doctrine:schema:drop', - ['--em' => $manager, '--force' => true, '--full-database' => true] - ); + runCommand($application, "doctrine:schema:drop --em={$manager} --force --full-database"); } } } diff --git a/src/Persistence/SymfonyCommandRunner.php b/src/Persistence/SymfonyCommandRunner.php deleted file mode 100644 index 76c61cf64..000000000 --- a/src/Persistence/SymfonyCommandRunner.php +++ /dev/null @@ -1,49 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Zenstruck\Foundry\Persistence; - -use Symfony\Bundle\FrameworkBundle\Console\Application; -use Symfony\Component\Console\Input\ArrayInput; -use Symfony\Component\Console\Output\BufferedOutput; -use Symfony\Component\HttpKernel\KernelInterface; - -/** - * @internal - * @author Nicolas PHILIPPE - */ -trait SymfonyCommandRunner -{ - /** - * @param array $parameters - */ - final protected static function runCommand(Application $application, string $command, array $parameters = [], bool $canFail = false): void - { - $exit = $application->run( - new ArrayInput(\array_merge(['command' => $command], $parameters + ['--no-interaction' => true])), - $output = new BufferedOutput() - ); - - if (0 !== $exit && !$canFail) { - throw new \RuntimeException(\sprintf('Error running "%s": %s', $command, $output->fetch())); - } - } - - final protected static function application(KernelInterface $kernel): Application - { - $application = new Application($kernel); - $application->setAutoExit(false); - - return $application; - } -} diff --git a/src/symfony_console.php b/src/symfony_console.php new file mode 100644 index 000000000..4ace905cd --- /dev/null +++ b/src/symfony_console.php @@ -0,0 +1,40 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry; + +use Symfony\Bundle\FrameworkBundle\Console\Application; +use Symfony\Component\Console\Input\StringInput; +use Symfony\Component\Console\Output\BufferedOutput; +use Symfony\Component\HttpKernel\KernelInterface; + +/** + * @internal + */ +function runCommand(Application $application, string $command, bool $canFail = false): void +{ + $exit = $application->run(new StringInput("$command --no-interaction"), $output = new BufferedOutput()); + + if (0 !== $exit && !$canFail) { + throw new \RuntimeException(\sprintf('Error running "%s": %s', $command, $output->fetch())); + } +} + +/** + * @internal + */ +function application(KernelInterface $kernel): Application +{ + $application = new Application($kernel); + $application->setAutoExit(false); + + return $application; +} diff --git a/tests/Fixture/CustomMigrations/Version20240611065130.php b/tests/Fixture/CustomMigrations/Version20240611065130.php deleted file mode 100644 index 9d51237ff..000000000 --- a/tests/Fixture/CustomMigrations/Version20240611065130.php +++ /dev/null @@ -1,44 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -// to "Migrations" directory on boot (cf. bootstrap.php) - -namespace Zenstruck\Foundry\Tests\Fixture\Migrations; - -use Doctrine\DBAL\Schema\Schema; -use Doctrine\Migrations\AbstractMigration; -use Zenstruck\Foundry\Tests\Fixture\EdgeCases\Migrate\ORM\EntityInAnotherSchema\Article; - -/** - * Create custom "cms" schema ({@see Article}) to ensure "migrate" mode is still working with multiple schemas. - * Note: the doctrine:migrations:diff command doesn't seem able to add this custom "CREATE SCHEMA" automatically. - * - * @see https://github.com/zenstruck/foundry/issues/618 - */ -final class Version20240611065130 extends AbstractMigration -{ - public function getDescription(): string - { - return 'Create custom "cms" schema.'; - } - - public function up(Schema $schema): void - { - $this->addSql('CREATE SCHEMA cms'); - } - - public function down(Schema $schema): void - { - $this->addSql('DROP SCHEMA cms'); - } -} diff --git a/tests/Fixture/Entity/Contact/StandardContact.php b/tests/Fixture/Entity/Contact/StandardContact.php index cd3f613a5..98fb91327 100644 --- a/tests/Fixture/Entity/Contact/StandardContact.php +++ b/tests/Fixture/Entity/Contact/StandardContact.php @@ -44,7 +44,7 @@ class StandardContact extends Contact #[ORM\JoinTable(name: 'category_tag_standard_secondary')] protected Collection $secondaryTags; - #[ORM\OneToOne(targetEntity: StandardAddress::class)] + #[ORM\OneToOne(targetEntity: StandardAddress::class, inversedBy: 'contact')] #[ORM\JoinColumn(nullable: false)] protected Address $address; } diff --git a/tests/Fixture/Entity/EdgeCases/ManyToOneToSelfReferencing/SelfReferencingInverseSide.php b/tests/Fixture/Entity/EdgeCases/ManyToOneToSelfReferencing/SelfReferencingInverseSide.php index b1f9c9d77..0219f6aa6 100644 --- a/tests/Fixture/Entity/EdgeCases/ManyToOneToSelfReferencing/SelfReferencingInverseSide.php +++ b/tests/Fixture/Entity/EdgeCases/ManyToOneToSelfReferencing/SelfReferencingInverseSide.php @@ -30,6 +30,6 @@ class SelfReferencingInverseSide #[ORM\ManyToOne()] public ?SelfReferencingInverseSide $inverseSide = null; - #[ORM\OneToOne(mappedBy: 'inverseSide')] + #[ORM\OneToMany(targetEntity: OwningSide::class, mappedBy: 'inverseSide')] public ?OwningSide $owningSide = null; } diff --git a/tests/Fixture/EdgeCases/Migrate/ORM/EntityInAnotherSchema/Article.php b/tests/Fixture/EntityInAnotherSchema/Article.php similarity index 68% rename from tests/Fixture/EdgeCases/Migrate/ORM/EntityInAnotherSchema/Article.php rename to tests/Fixture/EntityInAnotherSchema/Article.php index fba361efc..e95aa52b2 100644 --- a/tests/Fixture/EdgeCases/Migrate/ORM/EntityInAnotherSchema/Article.php +++ b/tests/Fixture/EntityInAnotherSchema/Article.php @@ -11,11 +11,17 @@ * file that was distributed with this source code. */ -namespace Zenstruck\Foundry\Tests\Fixture\EdgeCases\Migrate\ORM\EntityInAnotherSchema; +namespace Zenstruck\Foundry\Tests\Fixture\EntityInAnotherSchema; use Doctrine\ORM\Mapping as ORM; use Zenstruck\Foundry\Tests\Fixture\Model\Base; +/** + * Create custom "cms" schema ({@see Article}) to ensure "migrate" mode is still working with multiple schemas. + * Note: this entity is added to mapping only for PostgreSQ, as it is the only supported DBMS which handles multiple schemas. + * + * @see https://github.com/zenstruck/foundry/issues/618 + */ #[ORM\Entity] #[ORM\Table(name: 'article', schema: 'cms')] class Article extends Base diff --git a/tests/Fixture/MigrationTests/TestMigrationKernel.php b/tests/Fixture/MigrationTests/TestMigrationKernel.php new file mode 100644 index 000000000..a81d5e578 --- /dev/null +++ b/tests/Fixture/MigrationTests/TestMigrationKernel.php @@ -0,0 +1,119 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Tests\Fixture\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/MigrationTests/configs/migration-configuration-transactional.php b/tests/Fixture/MigrationTests/configs/migration-configuration-transactional.php new file mode 100644 index 000000000..f404363de --- /dev/null +++ b/tests/Fixture/MigrationTests/configs/migration-configuration-transactional.php @@ -0,0 +1,8 @@ + [ + 'Zenstruck\\Foundry\\Tests\\Fixture\\MigrationTests\\Migrations' => dirname(__DIR__).'/Migrations', + ], + 'transactional' => true, +]; diff --git a/tests/Fixture/MigrationTests/configs/migration-configuration.php b/tests/Fixture/MigrationTests/configs/migration-configuration.php new file mode 100644 index 000000000..965334962 --- /dev/null +++ b/tests/Fixture/MigrationTests/configs/migration-configuration.php @@ -0,0 +1,8 @@ + [ + 'Zenstruck\\Foundry\\Tests\\Fixture\\MigrationTests\\Migrations' => dirname(__DIR__).'/Migrations', + ], + 'transactional' => false, +]; diff --git a/tests/Fixture/TestKernel.php b/tests/Fixture/TestKernel.php index 589e903e2..66dfc1bbc 100644 --- a/tests/Fixture/TestKernel.php +++ b/tests/Fixture/TestKernel.php @@ -13,7 +13,6 @@ use DAMA\DoctrineTestBundle\DAMADoctrineTestBundle; use Doctrine\Bundle\DoctrineBundle\DoctrineBundle; -use Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle; use Doctrine\Bundle\MongoDBBundle\DoctrineMongoDBBundle; use Psr\Log\NullLogger; use Symfony\Bundle\FrameworkBundle\FrameworkBundle; @@ -45,7 +44,6 @@ public function registerBundles(): iterable if (\getenv('DATABASE_URL')) { yield new DoctrineBundle(); - yield new DoctrineMigrationsBundle(); } if (\getenv('MONGO_URL')) { @@ -75,7 +73,7 @@ protected function configureContainer(ContainerBuilder $c, LoaderInterface $load ], 'orm' => [ 'reset' => [ - 'mode' => \getenv('DATABASE_RESET_MODE') ?: ResetDatabaseMode::SCHEMA, + 'mode' => ResetDatabaseMode::SCHEMA, ], ], ]); @@ -101,30 +99,23 @@ protected function configureContainer(ContainerBuilder $c, LoaderInterface $load 'prefix' => 'Zenstruck\Foundry\Tests\Fixture\Model', 'alias' => 'Model', ], - ], - 'controller_resolver' => ['auto_mapping' => true], - ], - ]); - if (ResetDatabaseMode::MIGRATE->value === \getenv('DATABASE_RESET_MODE')) { - $c->loadFromExtension('doctrine', [ - 'orm' => [ - 'mappings' => [ - 'Migrate' => [ - 'is_bundle' => false, - 'type' => 'attribute', - 'dir' => '%kernel.project_dir%/tests/Fixture/EdgeCases/Migrate/ORM', - 'prefix' => 'Zenstruck\Foundry\Tests\Fixture\EdgeCases\Migrate\ORM', - 'alias' => 'Migrate', - ], - ], + // 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', + ] + ] + : [] + ), ], - ]); - } - - $c->loadFromExtension('doctrine_migrations', [ - 'migrations_paths' => [ - 'Zenstruck\\Foundry\\Tests\\Fixture\\Migrations' => '%kernel.project_dir%/tests/Fixture/Migrations', + 'controller_resolver' => ['auto_mapping' => false], ], ]); } diff --git a/tests/Integration/Migration/ResetDatabaseWithMigrationTest.php b/tests/Integration/Migration/ResetDatabaseWithMigrationTest.php new file mode 100644 index 000000000..cd1e1f922 --- /dev/null +++ b/tests/Integration/Migration/ResetDatabaseWithMigrationTest.php @@ -0,0 +1,97 @@ + + */ +final class ResetDatabaseWithMigrationTest extends KernelTestCase +{ + use Factories; + use ResetDatabase; + use RequiresORM; + + /** + * @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/bootstrap-migrate.php b/tests/bootstrap-migrate.php new file mode 100644 index 000000000..b319d7781 --- /dev/null +++ b/tests/bootstrap-migrate.php @@ -0,0 +1,48 @@ + + * + * 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\ErrorHandler\ErrorHandler; +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(); + +\set_exception_handler([new ErrorHandler(), 'handleException']); diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 03193ea6a..4d9bb4c61 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -9,15 +9,9 @@ * file that was distributed with this source code. */ -use Symfony\Bundle\FrameworkBundle\Console\Application; -use Symfony\Component\Console\Input\StringInput; -use Symfony\Component\Console\Output\NullOutput; use Symfony\Component\Dotenv\Dotenv; use Symfony\Component\ErrorHandler\ErrorHandler; use Symfony\Component\Filesystem\Filesystem; -use Symfony\Component\Finder\Finder; -use Zenstruck\Foundry\ORM\ResetDatabase\ResetDatabaseMode; -use Zenstruck\Foundry\Tests\Fixture\TestKernel; require \dirname(__DIR__).'/vendor/autoload.php'; @@ -27,29 +21,4 @@ (new Dotenv())->usePutenv()->loadEnv(__DIR__.'/../.env'); -if (\getenv('DATABASE_URL') && ResetDatabaseMode::MIGRATE->value === \getenv('DATABASE_RESET_MODE')) { - $fs->remove(__DIR__.'/Fixture/Migrations'); - $fs->mkdir(__DIR__.'/Fixture/Migrations'); - - $kernel = new TestKernel('test', true); - $kernel->boot(); - - $application = new Application($kernel); - $application->setAutoExit(false); - - $application->run(new StringInput('doctrine:database:drop --if-exists --force'), new NullOutput()); - $application->run(new StringInput('doctrine:database:create'), new NullOutput()); - $application->run(new StringInput('doctrine:migrations:diff'), new NullOutput()); - $application->run(new StringInput('doctrine:database:drop --force'), new NullOutput()); - - // restore custom migrations - // this must be after "doctrine:migrations:diff" otherwise - // Doctrine is not able to run its diff command - foreach ((new Finder())->files()->in(__DIR__.'/Fixture/CustomMigrations') as $customMigrationFile) { - $fs->copy($customMigrationFile->getRealPath(), __DIR__.'/Fixture/Migrations/'.$customMigrationFile->getFilename()); - } - - $kernel->shutdown(); -} - \set_exception_handler([new ErrorHandler(), 'handleException']); From a7bbf327db09307d392450c155761d1ee126df16 Mon Sep 17 00:00:00 2001 From: nikophil Date: Wed, 11 Dec 2024 18:09:18 +0000 Subject: [PATCH 037/102] bot: fix cs [skip ci] --- src/symfony_console.php | 2 +- .../Fixture/MigrationTests/TestMigrationKernel.php | 8 ++++---- .../migration-configuration-transactional.php | 11 ++++++++++- .../configs/migration-configuration.php | 11 ++++++++++- tests/Fixture/TestKernel.php | 4 ++-- .../Migration/ResetDatabaseWithMigrationTest.php | 13 +++++++++++-- tests/bootstrap-migrate.php | 12 ++++++------ 7 files changed, 44 insertions(+), 17 deletions(-) diff --git a/src/symfony_console.php b/src/symfony_console.php index 4ace905cd..d26197f57 100644 --- a/src/symfony_console.php +++ b/src/symfony_console.php @@ -21,7 +21,7 @@ */ function runCommand(Application $application, string $command, bool $canFail = false): void { - $exit = $application->run(new StringInput("$command --no-interaction"), $output = new BufferedOutput()); + $exit = $application->run(new StringInput("{$command} --no-interaction"), $output = new BufferedOutput()); if (0 !== $exit && !$canFail) { throw new \RuntimeException(\sprintf('Error running "%s": %s', $command, $output->fetch())); diff --git a/tests/Fixture/MigrationTests/TestMigrationKernel.php b/tests/Fixture/MigrationTests/TestMigrationKernel.php index a81d5e578..2038bb906 100644 --- a/tests/Fixture/MigrationTests/TestMigrationKernel.php +++ b/tests/Fixture/MigrationTests/TestMigrationKernel.php @@ -63,14 +63,14 @@ protected function configureContainer(ContainerBuilder $c, LoaderInterface $load 'reset' => [ 'mode' => ResetDatabaseMode::MIGRATE, 'migrations' => [ - 'configurations' => ($configFile = \getenv('WITH_MIGRATION_CONFIGURATION_FILE')) ? [$configFile] : [] + '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_migrations', include __DIR__.'/configs/migration-configuration.php'); } $c->loadFromExtension('doctrine', [ @@ -96,7 +96,7 @@ protected function configureContainer(ContainerBuilder $c, LoaderInterface $load // postgres acts weirdly with multiple schemas // @see https://github.com/doctrine/DoctrineBundle/issues/548 - ...(str_starts_with(\getenv('DATABASE_URL') ?: '', 'postgresql') + ...(\str_starts_with(\getenv('DATABASE_URL') ?: '', 'postgresql') ? [ 'EntityInAnotherSchema' => [ 'is_bundle' => false, @@ -104,7 +104,7 @@ protected function configureContainer(ContainerBuilder $c, LoaderInterface $load 'dir' => '%kernel.project_dir%/tests/Fixture/EntityInAnotherSchema', 'prefix' => 'Zenstruck\Foundry\Tests\Fixture\EntityInAnotherSchema', 'alias' => 'Migrate', - ] + ], ] : [] ), diff --git a/tests/Fixture/MigrationTests/configs/migration-configuration-transactional.php b/tests/Fixture/MigrationTests/configs/migration-configuration-transactional.php index f404363de..cb0928594 100644 --- a/tests/Fixture/MigrationTests/configs/migration-configuration-transactional.php +++ b/tests/Fixture/MigrationTests/configs/migration-configuration-transactional.php @@ -1,8 +1,17 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + return [ 'migrations_paths' => [ - 'Zenstruck\\Foundry\\Tests\\Fixture\\MigrationTests\\Migrations' => dirname(__DIR__).'/Migrations', + 'Zenstruck\\Foundry\\Tests\\Fixture\\MigrationTests\\Migrations' => \dirname(__DIR__).'/Migrations', ], 'transactional' => true, ]; diff --git a/tests/Fixture/MigrationTests/configs/migration-configuration.php b/tests/Fixture/MigrationTests/configs/migration-configuration.php index 965334962..def58e6f5 100644 --- a/tests/Fixture/MigrationTests/configs/migration-configuration.php +++ b/tests/Fixture/MigrationTests/configs/migration-configuration.php @@ -1,8 +1,17 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + return [ 'migrations_paths' => [ - 'Zenstruck\\Foundry\\Tests\\Fixture\\MigrationTests\\Migrations' => dirname(__DIR__).'/Migrations', + 'Zenstruck\\Foundry\\Tests\\Fixture\\MigrationTests\\Migrations' => \dirname(__DIR__).'/Migrations', ], 'transactional' => false, ]; diff --git a/tests/Fixture/TestKernel.php b/tests/Fixture/TestKernel.php index 66dfc1bbc..182f63e6d 100644 --- a/tests/Fixture/TestKernel.php +++ b/tests/Fixture/TestKernel.php @@ -102,7 +102,7 @@ protected function configureContainer(ContainerBuilder $c, LoaderInterface $load // postgres acts weirdly with multiple schemas // @see https://github.com/doctrine/DoctrineBundle/issues/548 - ...(str_starts_with(\getenv('DATABASE_URL'), 'postgresql') + ...(\str_starts_with(\getenv('DATABASE_URL'), 'postgresql') ? [ 'EntityInAnotherSchema' => [ 'is_bundle' => false, @@ -110,7 +110,7 @@ protected function configureContainer(ContainerBuilder $c, LoaderInterface $load 'dir' => '%kernel.project_dir%/tests/Fixture/EntityInAnotherSchema', 'prefix' => 'Zenstruck\Foundry\Tests\Fixture\EntityInAnotherSchema', 'alias' => 'Migrate', - ] + ], ] : [] ), diff --git a/tests/Integration/Migration/ResetDatabaseWithMigrationTest.php b/tests/Integration/Migration/ResetDatabaseWithMigrationTest.php index cd1e1f922..c4e5cfc02 100644 --- a/tests/Integration/Migration/ResetDatabaseWithMigrationTest.php +++ b/tests/Integration/Migration/ResetDatabaseWithMigrationTest.php @@ -2,6 +2,15 @@ declare(strict_types=1); +/* + * This file is part of the zenstruck/foundry package. + * + * (c) Kevin Bond + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Zenstruck\Foundry\Tests\Integration\Migration; use Symfony\Bundle\FrameworkBundle\Console\Application; @@ -25,8 +34,8 @@ final class ResetDatabaseWithMigrationTest extends KernelTestCase { use Factories; - use ResetDatabase; use RequiresORM; + use ResetDatabase; /** * @test @@ -82,7 +91,7 @@ public function global_objects_are_created(): void */ public function can_create_object_in_another_schema(): void { - if (!str_starts_with(\getenv('DATABASE_URL') ?: '', 'postgresql')) { + if (!\str_starts_with(\getenv('DATABASE_URL') ?: '', 'postgresql')) { self::markTestSkipped('PostgreSQL needed.'); } diff --git a/tests/bootstrap-migrate.php b/tests/bootstrap-migrate.php index b319d7781..582fae7bc 100644 --- a/tests/bootstrap-migrate.php +++ b/tests/bootstrap-migrate.php @@ -17,16 +17,16 @@ use function Zenstruck\Foundry\application; use function Zenstruck\Foundry\runCommand; -require \dirname(__DIR__) . '/vendor/autoload.php'; +require \dirname(__DIR__).'/vendor/autoload.php'; $fs = new Filesystem(); $fs->remove(__DIR__.'/../var'); -(new Dotenv())->usePutenv()->loadEnv(__DIR__ . '/../.env'); +(new Dotenv())->usePutenv()->loadEnv(__DIR__.'/../.env'); -$fs->remove(__DIR__ . '/Fixture/MigrationTests/Migrations'); -$fs->mkdir(__DIR__ . '/Fixture/MigrationTests/Migrations'); +$fs->remove(__DIR__.'/Fixture/MigrationTests/Migrations'); +$fs->mkdir(__DIR__.'/Fixture/MigrationTests/Migrations'); $kernel = new TestMigrationKernel('test', true); $kernel->boot(); @@ -37,8 +37,8 @@ runCommand($application, 'doctrine:database:create', canFail: true); $configuration = ''; -if (getenv('WITH_MIGRATION_CONFIGURATION_FILE')) { - $configuration = '--configuration '.getcwd().'/'.getenv('WITH_MIGRATION_CONFIGURATION_FILE'); +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); From 138801de27fa409b324588ffbda52071c108b1c5 Mon Sep 17 00:00:00 2001 From: Nicolas PHILIPPE Date: Thu, 12 Dec 2024 10:02:38 +0100 Subject: [PATCH 038/102] chore: remove error handler hack (#729) --- composer.json | 5 +++-- src/Test/ResetDatabase.php | 7 +------ src/phpunit_helper.php | 42 ------------------------------------- tests/bootstrap-migrate.php | 3 --- tests/bootstrap.php | 9 +++----- 5 files changed, 7 insertions(+), 59 deletions(-) delete mode 100644 src/phpunit_helper.php diff --git a/composer.json b/composer.json index 93efd2223..490e79bb4 100644 --- a/composer.json +++ b/composer.json @@ -40,6 +40,7 @@ "symfony/dotenv": "^6.4|^7.0", "symfony/maker-bundle": "^1.55", "symfony/phpunit-bridge": "^6.4|^7.0", + "symfony/runtime": "^6.4|^7.0", "symfony/translation-contracts": "^3.4", "symfony/var-dumper": "^6.4|^7.0", "symfony/yaml": "^6.4|^7.0" @@ -52,7 +53,6 @@ "files": [ "src/functions.php", "src/Persistence/functions.php", - "src/phpunit_helper.php", "src/symfony_console.php" ] }, @@ -68,7 +68,8 @@ "sort-packages": true, "allow-plugins": { "bamarni/composer-bin-plugin": true, - "symfony/flex": true + "symfony/flex": true, + "symfony/runtime": false } }, "extra": { diff --git a/src/Test/ResetDatabase.php b/src/Test/ResetDatabase.php index 8b7ee3d79..d9698b817 100644 --- a/src/Test/ResetDatabase.php +++ b/src/Test/ResetDatabase.php @@ -16,8 +16,6 @@ use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; use Zenstruck\Foundry\Persistence\ResetDatabase\ResetDatabaseManager; -use function Zenstruck\Foundry\restorePhpUnitErrorHandler; - /** * @author Kevin Bond */ @@ -36,10 +34,7 @@ public static function _resetDatabaseBeforeFirstTest(): void ResetDatabaseManager::resetBeforeFirstTest( static fn() => static::bootKernel(), - static function(): void { - static::ensureKernelShutdown(); - restorePhpUnitErrorHandler(); - }, + static fn() => static::ensureKernelShutdown(), ); } diff --git a/src/phpunit_helper.php b/src/phpunit_helper.php deleted file mode 100644 index 1e5a7a2fe..000000000 --- a/src/phpunit_helper.php +++ /dev/null @@ -1,42 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Zenstruck\Foundry; - -/** - * When using ResetDatabase trait, we're booting the kernel, - * which registers the Symfony's error handler too soon. - * It is then impossible for PHPUnit to handle deprecations. - * - * This method tries to mitigate this problem by restoring the error handler. - * - * @see https://github.com/symfony/symfony/issues/53812 - * - * @internal - */ -function restorePhpUnitErrorHandler(): void -{ - if (!\class_exists(\PHPUnit\Runner\ErrorHandler::class)) { - return; - } - - while (true) { - $previousHandler = \set_error_handler(static fn() => null); // @phpstan-ignore argument.type - \restore_error_handler(); - $isPhpUnitErrorHandler = $previousHandler instanceof \PHPUnit\Runner\ErrorHandler; - if (null === $previousHandler || $isPhpUnitErrorHandler) { - break; - } - \restore_error_handler(); - } -} diff --git a/tests/bootstrap-migrate.php b/tests/bootstrap-migrate.php index 582fae7bc..79e2e7b7c 100644 --- a/tests/bootstrap-migrate.php +++ b/tests/bootstrap-migrate.php @@ -10,7 +10,6 @@ */ use Symfony\Component\Dotenv\Dotenv; -use Symfony\Component\ErrorHandler\ErrorHandler; use Symfony\Component\Filesystem\Filesystem; use Zenstruck\Foundry\Tests\Fixture\MigrationTests\TestMigrationKernel; @@ -44,5 +43,3 @@ runCommand($application, 'doctrine:database:drop --force', canFail: true); $kernel->shutdown(); - -\set_exception_handler([new ErrorHandler(), 'handleException']); diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 4d9bb4c61..a1b7ae6c9 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -10,15 +10,12 @@ */ use Symfony\Component\Dotenv\Dotenv; -use Symfony\Component\ErrorHandler\ErrorHandler; 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'); -(new Dotenv())->usePutenv()->loadEnv(__DIR__.'/../.env'); - -\set_exception_handler([new ErrorHandler(), 'handleException']); +(new Dotenv())->usePutenv()->loadEnv(__DIR__ . '/../.env'); From 6f744b05706c1780e61368f23bf77e18c83d8fcd Mon Sep 17 00:00:00 2001 From: Nicolas PHILIPPE Date: Thu, 12 Dec 2024 10:03:06 +0100 Subject: [PATCH 039/102] changelog: update [skip ci] --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 811863870..eeca19095 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # CHANGELOG +## [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) + +* 138801d chore: remove error handler hack (#729) by @nikophil +* cd9dbf5 refactor: extract reset:migration tests in another testsuite (#692) by @nikophil + ## [v2.3.0](https://github.com/zenstruck/foundry/releases/tag/v2.3.0) December 11th, 2024 - [v2.2.2...v2.3.0](https://github.com/zenstruck/foundry/compare/v2.2.2...v2.3.0) From 3e5023c8af32fedc632bab5839014e0ea9b4ea57 Mon Sep 17 00:00:00 2001 From: nikophil Date: Thu, 12 Dec 2024 09:05:38 +0000 Subject: [PATCH 040/102] bot: fix cs [skip ci] --- tests/bootstrap.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/bootstrap.php b/tests/bootstrap.php index a1b7ae6c9..363b11221 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'); -(new Dotenv())->usePutenv()->loadEnv(__DIR__ . '/../.env'); +(new Dotenv())->usePutenv()->loadEnv(__DIR__.'/../.env'); From ea895046e6bd5ef260d3e776483cddc024ce618b Mon Sep 17 00:00:00 2001 From: Nicolas PHILIPPE Date: Sat, 14 Dec 2024 13:07:13 +0100 Subject: [PATCH 041/102] fix: pass to `afterPersist` hook the attributes from `beforeInstantiate` (#745) * bug: attributes added in `beforeInstantiate` are not passed to `afterPersist` * fix: attributes added in are not passed to --------- Co-authored-by: Kevin Bond --- src/Factory.php | 28 +++++++++++++------ src/Persistence/PersistentObjectFactory.php | 2 +- tests/Integration/ObjectFactoryTest.php | 16 +++++++++++ .../GenericProxyFactoryTestCase.php | 21 ++++++++++++++ 4 files changed, 57 insertions(+), 10 deletions(-) diff --git a/src/Factory.php b/src/Factory.php index 4707fa70b..787ddd0a1 100644 --- a/src/Factory.php +++ b/src/Factory.php @@ -28,6 +28,14 @@ abstract class Factory /** @phpstan-var Attributes[] */ private array $attributes; + /** + * Memoization of normalized parameters + * + * @internal + * @var Parameters|null + */ + protected array|null $normalizedParameters = null; + // keep an empty constructor for BC public function __construct() { @@ -148,6 +156,16 @@ final protected static function faker(): Faker\Generator return Configuration::instance()->faker; } + /** + * Override to adjust default attributes & config. + * + * @return static + */ + protected function initialize(): static + { + return $this; + } + /** * @internal * @@ -174,14 +192,6 @@ final protected function normalizeAttributes(array|callable $attributes = []): a ); } - /** - * Override to adjust default attributes & config. - */ - protected function initialize(): static - { - return $this; - } - /** * @internal * @@ -191,7 +201,7 @@ protected function initialize(): static */ protected function normalizeParameters(array $parameters): array { - return array_combine( + return $this->normalizedParameters = array_combine( array_keys($parameters), \array_map($this->normalizeParameter(...), array_keys($parameters), $parameters) ); diff --git a/src/Persistence/PersistentObjectFactory.php b/src/Persistence/PersistentObjectFactory.php index f2aabca44..d9cfdc968 100644 --- a/src/Persistence/PersistentObjectFactory.php +++ b/src/Persistence/PersistentObjectFactory.php @@ -217,7 +217,7 @@ public function create(callable|array $attributes = []): 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); 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/GenericProxyFactoryTestCase.php b/tests/Integration/Persistence/GenericProxyFactoryTestCase.php index c8af22efb..b8cdd16c8 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 */ From 2dcad10aaebad5c2348fbd526cd6a51b5f198e33 Mon Sep 17 00:00:00 2001 From: Nicolas PHILIPPE Date: Sat, 14 Dec 2024 13:08:32 +0100 Subject: [PATCH 042/102] feat: provide current factory to hook (#738) --- docs/index.rst | 10 ++-- src/ObjectFactory.php | 12 ++--- src/Persistence/PersistentObjectFactory.php | 6 +-- .../WithHooksInInitializeFactory.php | 48 +++++++++++++++++++ .../FactoryWithHooksInInitializeTest.php | 27 +++++++++++ 5 files changed, 91 insertions(+), 12 deletions(-) create mode 100644 tests/Fixture/Factories/WithHooksInInitializeFactory.php create mode 100644 tests/Integration/Persistence/FactoryWithHooksInInitializeTest.php diff --git a/docs/index.rst b/docs/index.rst index 9a38cdff0..b341eb915 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -552,20 +552,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 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/Persistence/PersistentObjectFactory.php b/src/Persistence/PersistentObjectFactory.php index d9cfdc968..0c2cf8ef8 100644 --- a/src/Persistence/PersistentObjectFactory.php +++ b/src/Persistence/PersistentObjectFactory.php @@ -36,7 +36,7 @@ abstract class PersistentObjectFactory extends ObjectFactory { private bool $persist; - /** @phpstan-var list */ + /** @phpstan-var list */ private array $afterPersist = []; /** @var list */ @@ -220,7 +220,7 @@ public function create(callable|array $attributes = []): object $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); @@ -246,7 +246,7 @@ final public function withoutPersisting(): static } /** - * @phpstan-param callable(T, Parameters):void $callback + * @phpstan-param callable(T, Parameters, static):void $callback */ final public function afterPersist(callable $callback): static { diff --git a/tests/Fixture/Factories/WithHooksInInitializeFactory.php b/tests/Fixture/Factories/WithHooksInInitializeFactory.php new file mode 100644 index 000000000..fc7d930e0 --- /dev/null +++ b/tests/Fixture/Factories/WithHooksInInitializeFactory.php @@ -0,0 +1,48 @@ + + * @extends PersistentObjectFactory + */ +final class WithHooksInInitializeFactory extends PersistentObjectFactory +{ + protected function defaults(): array|callable + { + return [ + 'city' => self::faker()->city(), + ]; + } + + public static function class(): string + { + return StandardAddress::class; + } + + protected function initialize(): static + { + return $this + ->beforeInstantiate( + function (array $parameters, string $class, WithHooksInInitializeFactory $factory) { + if (!$factory->isPersisting()) { + $parameters['city'] = 'beforeInstantiate'; + } + + return $parameters; + } + ) + ->afterInstantiate( + function (StandardAddress $object, array $parameters, WithHooksInInitializeFactory $factory) { + if (!$factory->isPersisting()) { + $object->setCity("{$object->getCity()} - afterInstantiate"); + } + } + ); + } +} diff --git a/tests/Integration/Persistence/FactoryWithHooksInInitializeTest.php b/tests/Integration/Persistence/FactoryWithHooksInInitializeTest.php new file mode 100644 index 000000000..708ee9909 --- /dev/null +++ b/tests/Integration/Persistence/FactoryWithHooksInInitializeTest.php @@ -0,0 +1,27 @@ + + */ +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()); + } +} From 98f018c24c239b949654ba85e71cb54c0122ade9 Mon Sep 17 00:00:00 2001 From: Nicolas PHILIPPE Date: Sat, 14 Dec 2024 13:08:54 +0100 Subject: [PATCH 043/102] feat: schedule objects for insert right after instantiation (#742) * feat: provide current factory to hook * feat: schedule objects for insert right after instantiation --- src/Factory.php | 13 +++- src/ORM/OrmV2PersistenceStrategy.php | 34 ++++----- src/ORM/OrmV3PersistenceStrategy.php | 33 ++++----- ...ta.php => InverseRelationshipMetadata.php} | 6 +- src/Persistence/PersistenceManager.php | 23 +++++- src/Persistence/PersistenceStrategy.php | 2 +- src/Persistence/PersistentObjectFactory.php | 39 +++++++--- .../CascadeEntityFactoryRelationshipTest.php | 72 ------------------- .../ORM/EntityFactoryRelationshipTestCase.php | 23 ++++++ ...xyGenericEntityRepositoryDecoratorTest.php | 2 +- 10 files changed, 115 insertions(+), 132 deletions(-) rename src/Persistence/{RelationshipMetadata.php => InverseRelationshipMetadata.php} (72%) diff --git a/src/Factory.php b/src/Factory.php index 787ddd0a1..78bb6fb57 100644 --- a/src/Factory.php +++ b/src/Factory.php @@ -58,7 +58,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); } /** @@ -192,6 +195,14 @@ final protected function normalizeAttributes(array|callable $attributes = []): a ); } + /** + * @internal + */ + protected function initializeInternal(): static + { + return $this; + } + /** * @internal * 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..c84888fb9 100644 --- a/src/ORM/OrmV3PersistenceStrategy.php +++ b/src/ORM/OrmV3PersistenceStrategy.php @@ -20,50 +20,43 @@ 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/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/PersistenceManager.php b/src/Persistence/PersistenceManager.php index 88979e6d2..6ab3643cb 100644 --- a/src/Persistence/PersistenceManager.php +++ b/src/Persistence/PersistenceManager.php @@ -75,6 +75,25 @@ 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; + } + /** * @template T * @@ -212,12 +231,12 @@ 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); } /** diff --git a/src/Persistence/PersistenceStrategy.php b/src/Persistence/PersistenceStrategy.php index 975ce2dd9..3e911f491 100644 --- a/src/Persistence/PersistenceStrategy.php +++ b/src/Persistence/PersistenceStrategy.php @@ -63,7 +63,7 @@ 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; } diff --git a/src/Persistence/PersistentObjectFactory.php b/src/Persistence/PersistentObjectFactory.php index 0c2cf8ef8..bdda8572c 100644 --- a/src/Persistence/PersistentObjectFactory.php +++ b/src/Persistence/PersistentObjectFactory.php @@ -263,19 +263,20 @@ protected function normalizeParameter(string $field, mixed $value): mixed } if ($value instanceof self && isset($this->persist)) { - $value->persist = $this->persist; // todo - breaks immutability + $value = $this->isPersisting() + ? $value->andPersist() + : $value->withoutPersisting(); } 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) { + if ($inversedRelationshipMetadata && !$inversedRelationshipMetadata->isCollection) { + $inverseField = $inversedRelationshipMetadata->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()); @@ -294,10 +295,6 @@ protected function normalizeParameter(string $field, mixed $value): mixed return $inversedObject; } - - if (Configuration::instance()->persistence()->relationshipMetadata(static::class(), $value::class(), $field)?->isCascadePersist) { - $value->persist = false; - } } return unproxy(parent::normalizeParameter($field, $value)); @@ -311,7 +308,11 @@ protected function normalizeCollection(string $field, FactoryCollection $collect $pm = Configuration::instance()->persistence(); - if ($inverseField = $pm->relationshipMetadata($collection->factory::class(), static::class(), $field)?->inverseField) { + $inverseRelationshipMetadata = $pm->inverseRelationshipMetadata(static::class(), $collection->factory::class(), $field); + + if ($inverseRelationshipMetadata && $inverseRelationshipMetadata->isCollection) { + $inverseField = $inverseRelationshipMetadata->inverseField; + $this->tempAfterPersist[] = static function(object $object) use ($collection, $inverseField, $pm) { $collection->create([$inverseField => $object]); $pm->refresh($object); @@ -364,6 +365,22 @@ final protected function isPersisting(): bool return $this->persist ?? $config->isPersistenceAvailable() && $config->persistence()->isEnabled() && $config->persistence()->autoPersist(static::class()); } + /** + * Schedule any new object for insert right after instantiation + */ + 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 { $configuration = Configuration::instance(); diff --git a/tests/Integration/ORM/CascadeEntityFactoryRelationshipTest.php b/tests/Integration/ORM/CascadeEntityFactoryRelationshipTest.php index a0cc97804..8a2822d67 100644 --- a/tests/Integration/ORM/CascadeEntityFactoryRelationshipTest.php +++ b/tests/Integration/ORM/CascadeEntityFactoryRelationshipTest.php @@ -22,78 +22,6 @@ */ 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 diff --git a/tests/Integration/ORM/EntityFactoryRelationshipTestCase.php b/tests/Integration/ORM/EntityFactoryRelationshipTestCase.php index 2e7c05e87..72761bc82 100644 --- a/tests/Integration/ORM/EntityFactoryRelationshipTestCase.php +++ b/tests/Integration/ORM/EntityFactoryRelationshipTestCase.php @@ -333,6 +333,29 @@ public function forced_one_to_many_with_doctrine_collection_type(): void static::categoryFactory()::assert()->count(1); } + /** + * @test + */ + public function ensure_one_to_many_cascade_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); + } + } + /** * @return PersistentObjectFactory */ 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; From c17ef91158e8b014ad58ce3c028a58ce125188f3 Mon Sep 17 00:00:00 2001 From: Nicolas PHILIPPE Date: Sat, 14 Dec 2024 13:09:17 +0100 Subject: [PATCH 044/102] fix: define FactoryCollection type more precisely (#744) --- src/Factory.php | 14 ++++----- src/FactoryCollection.php | 31 ++++++++++--------- stubs/phpstan/ObjectFactory.php | 8 +++-- stubs/phpstan/PersistentObjectFactory.php | 8 +++-- .../phpstan/PersistentProxyObjectFactory.php | 8 +++-- stubs/psalm/ObjectFactory.php | 8 +++-- stubs/psalm/PersistentObjectFactory.php | 8 +++-- stubs/psalm/PersistentProxyObjectFactory.php | 9 ++++-- .../ORM/EntityFactoryRelationshipTestCase.php | 2 +- 9 files changed, 55 insertions(+), 41 deletions(-) diff --git a/src/Factory.php b/src/Factory.php index 78bb6fb57..8b4f18a1f 100644 --- a/src/Factory.php +++ b/src/Factory.php @@ -112,24 +112,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 { @@ -137,7 +137,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 } /** @@ -252,7 +252,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..d2f70c763 100644 --- a/src/FactoryCollection.php +++ b/src/FactoryCollection.php @@ -15,22 +15,23 @@ * @author Kevin Bond * * @template T - * @implements \IteratorAggregate> + * @template TFactory of Factory + * @implements \IteratorAggregate * * @phpstan-import-type Attributes from Factory */ final class FactoryCollection implements \IteratorAggregate { /** - * @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) { } /** - * @phpstan-assert-if-true non-empty-list> $potentialFactories + * @phpstan-assert-if-true non-empty-list $potentialFactories * * @internal */ @@ -55,9 +56,9 @@ public static function accepts(mixed $potentialFactories): bool } /** - * @param array $factories + * @param array $factories * - * @return self + * @return self * * @internal */ @@ -71,9 +72,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 +82,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 +96,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 +116,7 @@ public function create(array|callable $attributes = []): array } /** - * @return list> + * @return list */ public function all(): array { @@ -132,7 +133,7 @@ public function all(): array $factories[] = $this->factory->with($attributesOrFactory)->with(['__index' => $i++]); } - return $factories; + return $factories; // @phpstan-ignore return.type (PHPStan does not understand we have an array of factories) } public function getIterator(): \Traversable @@ -141,7 +142,7 @@ public function getIterator(): \Traversable } /** - * @return iterable}> + * @return iterable */ public function asDataProvider(): iterable { diff --git a/stubs/phpstan/ObjectFactory.php b/stubs/phpstan/ObjectFactory.php index a90cb8e19..c7d1fd026 100644 --- a/stubs/phpstan/ObjectFactory.php +++ b/stubs/phpstan/ObjectFactory.php @@ -47,12 +47,14 @@ protected function defaults(): array|callable // methods with FactoryCollection $factoryCollection = FactoryCollection::class; -assertType("{$factoryCollection}", UserObjectFactory::new()->many(2)); -assertType("{$factoryCollection}", UserObjectFactory::new()->range(1, 2)); -assertType("{$factoryCollection}", UserObjectFactory::new()->sequence([])); +$factory = UserObjectFactory::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()); +assertType("array", 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..66af814c8 100644 --- a/stubs/phpstan/PersistentObjectFactory.php +++ b/stubs/phpstan/PersistentObjectFactory.php @@ -53,12 +53,14 @@ protected function defaults(): array|callable // methods with FactoryCollection $factoryCollection = FactoryCollection::class; -assertType("{$factoryCollection}", UserFactory::new()->many(2)); -assertType("{$factoryCollection}", UserFactory::new()->range(1, 2)); -assertType("{$factoryCollection}", UserFactory::new()->sequence([])); +$factory = UserFactory::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()); +assertType("array", UserFactory::new()->many(2)->all()); // methods using repository() $repository = UserFactory::repository(); diff --git a/stubs/phpstan/PersistentProxyObjectFactory.php b/stubs/phpstan/PersistentProxyObjectFactory.php index c5768d3d5..b8044aac3 100644 --- a/stubs/phpstan/PersistentProxyObjectFactory.php +++ b/stubs/phpstan/PersistentProxyObjectFactory.php @@ -54,12 +54,14 @@ protected function defaults(): array|callable // 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([])); +$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("array", UserProxyFactory::new()->many(2)->create()); assertType("array", UserProxyFactory::new()->range(1, 2)->create()); assertType("array", UserProxyFactory::new()->sequence([])->create()); +assertType("array", UserProxyFactory::new()->many(2)->all()); // methods using repository() $repository = UserProxyFactory::repository(); 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..6a6b471fb 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(); diff --git a/stubs/psalm/PersistentProxyObjectFactory.php b/stubs/psalm/PersistentProxyObjectFactory.php index 10a8f4e5f..51b968e6e 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(); diff --git a/tests/Integration/ORM/EntityFactoryRelationshipTestCase.php b/tests/Integration/ORM/EntityFactoryRelationshipTestCase.php index 72761bc82..1ccea746e 100644 --- a/tests/Integration/ORM/EntityFactoryRelationshipTestCase.php +++ b/tests/Integration/ORM/EntityFactoryRelationshipTestCase.php @@ -91,7 +91,7 @@ public function disabling_persistence_cascades_to_children(): void /** * @test - * @param FactoryCollection|list> $contacts + * @param FactoryCollection>|list> $contacts * @dataProvider one_to_many_provider */ public function one_to_many(FactoryCollection|array $contacts): void From d46672c8b69e13a04108423c4666757765ac409e Mon Sep 17 00:00:00 2001 From: nikophil Date: Sat, 14 Dec 2024 12:09:38 +0000 Subject: [PATCH 045/102] bot: fix cs [skip ci] --- src/ORM/OrmV3PersistenceStrategy.php | 1 - src/Persistence/PersistentObjectFactory.php | 4 ++-- .../WithHooksInInitializeFactory.php | 23 +++++++++++++------ .../FactoryWithHooksInInitializeTest.php | 9 ++++++++ 4 files changed, 27 insertions(+), 10 deletions(-) diff --git a/src/ORM/OrmV3PersistenceStrategy.php b/src/ORM/OrmV3PersistenceStrategy.php index c84888fb9..6d8d187e3 100644 --- a/src/ORM/OrmV3PersistenceStrategy.php +++ b/src/ORM/OrmV3PersistenceStrategy.php @@ -17,7 +17,6 @@ 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\InverseRelationshipMetadata; diff --git a/src/Persistence/PersistentObjectFactory.php b/src/Persistence/PersistentObjectFactory.php index bdda8572c..234bd0449 100644 --- a/src/Persistence/PersistentObjectFactory.php +++ b/src/Persistence/PersistentObjectFactory.php @@ -366,12 +366,12 @@ final protected function isPersisting(): bool } /** - * Schedule any new object for insert right after instantiation + * Schedule any new object for insert right after instantiation. */ final protected function initializeInternal(): static { return $this->afterInstantiate( - static function (object $object, array $parameters, PersistentObjectFactory $factory): void { + static function(object $object, array $parameters, PersistentObjectFactory $factory): void { if (!$factory->isPersisting()) { return; } diff --git a/tests/Fixture/Factories/WithHooksInInitializeFactory.php b/tests/Fixture/Factories/WithHooksInInitializeFactory.php index fc7d930e0..faa389f1f 100644 --- a/tests/Fixture/Factories/WithHooksInInitializeFactory.php +++ b/tests/Fixture/Factories/WithHooksInInitializeFactory.php @@ -2,6 +2,15 @@ declare(strict_types=1); +/* + * This file is part of the zenstruck/foundry package. + * + * (c) Kevin Bond + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Zenstruck\Foundry\Tests\Fixture\Factories; use Zenstruck\Foundry\Persistence\PersistentObjectFactory; @@ -13,6 +22,11 @@ */ final class WithHooksInInitializeFactory extends PersistentObjectFactory { + public static function class(): string + { + return StandardAddress::class; + } + protected function defaults(): array|callable { return [ @@ -20,16 +34,11 @@ protected function defaults(): array|callable ]; } - public static function class(): string - { - return StandardAddress::class; - } - protected function initialize(): static { return $this ->beforeInstantiate( - function (array $parameters, string $class, WithHooksInInitializeFactory $factory) { + function(array $parameters, string $class, WithHooksInInitializeFactory $factory) { if (!$factory->isPersisting()) { $parameters['city'] = 'beforeInstantiate'; } @@ -38,7 +47,7 @@ function (array $parameters, string $class, WithHooksInInitializeFactory $factor } ) ->afterInstantiate( - function (StandardAddress $object, array $parameters, WithHooksInInitializeFactory $factory) { + function(StandardAddress $object, array $parameters, WithHooksInInitializeFactory $factory) { if (!$factory->isPersisting()) { $object->setCity("{$object->getCity()} - afterInstantiate"); } diff --git a/tests/Integration/Persistence/FactoryWithHooksInInitializeTest.php b/tests/Integration/Persistence/FactoryWithHooksInInitializeTest.php index 708ee9909..4cb182a76 100644 --- a/tests/Integration/Persistence/FactoryWithHooksInInitializeTest.php +++ b/tests/Integration/Persistence/FactoryWithHooksInInitializeTest.php @@ -2,6 +2,15 @@ declare(strict_types=1); +/* + * This file is part of the zenstruck/foundry package. + * + * (c) Kevin Bond + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Zenstruck\Foundry\Tests\Integration\Persistence; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; From f4ba5d807eec63c3a6e89dae0d0bb2f7f50c8834 Mon Sep 17 00:00:00 2001 From: Nicolas PHILIPPE Date: Sat, 14 Dec 2024 15:12:20 +0100 Subject: [PATCH 046/102] tests: add CI permutation with windows (#747) --- src/ORM/ResetDatabase/BaseOrmResetter.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/ORM/ResetDatabase/BaseOrmResetter.php b/src/ORM/ResetDatabase/BaseOrmResetter.php index 8f2bf840e..c1a2a3d4a 100644 --- a/src/ORM/ResetDatabase/BaseOrmResetter.php +++ b/src/ORM/ResetDatabase/BaseOrmResetter.php @@ -60,9 +60,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 (DoctrineOrmVersionGuesser::isOrmV3() && $dbPath && (new Filesystem())->exists($dbPath)) { + file_put_contents($dbPath, ''); } continue; From 23b4ec44d45f0d775fbe06b3b79bae7b5b084d33 Mon Sep 17 00:00:00 2001 From: Nicolas PHILIPPE Date: Sat, 14 Dec 2024 22:16:58 +0100 Subject: [PATCH 047/102] tests: automatically create cascade persist permutations (#666) * tests: automatically create cascade persist permutations * refactor: remove unneeded tests * refactor: remove unneeded factories and entities --- .github/workflows/ci.yml | 28 +- bin/tools/phpstan/composer.lock | 64 ++-- phpunit | 2 +- phpunit-paratest.xml.dist | 1 + ...cadePersistOnLoadClassMetadataListener.php | 41 ++ ...hangesEntityRelationshipCascadePersist.php | 119 ++++++ .../DoctrineCascadeRelationshipMetadata.php | 63 ++++ .../PhpUnitTestExtension.php | 40 ++ .../UsingRelationships.php | 20 + tests/Fixture/Entity/Address.php | 5 +- .../Fixture/Entity/Address/CascadeAddress.php | 27 -- .../Entity/Address/StandardAddress.php | 27 -- tests/Fixture/Entity/Category.php | 6 +- .../Entity/Category/CascadeCategory.php | 30 -- .../Entity/Category/StandardCategory.php | 30 -- .../Entity/{Contact => }/ChildContact.php | 4 +- tests/Fixture/Entity/Contact.php | 15 +- .../Fixture/Entity/Contact/CascadeContact.php | 46 --- .../Entity/Contact/StandardContact.php | 50 --- .../CascadeRelationshipWithGlobalEntity.php | 27 -- .../RelationshipWithGlobalEntity.php | 11 +- .../StandardRelationshipWithGlobalEntity.php | 27 -- .../CascadeInversedSideEntity.php | 24 -- .../CascadeOwningSideEntity.php | 29 -- ...nversedSideEntity.php => InversedSide.php} | 15 +- .../{OwningSideEntity.php => OwningSide.php} | 11 +- .../StandardInversedSideEntity.php | 24 -- .../StandardOwningSideEntity.php | 29 -- tests/Fixture/Entity/Tag.php | 6 +- tests/Fixture/Entity/Tag/CascadeTag.php | 30 -- tests/Fixture/Entity/Tag/StandardTag.php | 30 -- ...eAddressFactory.php => AddressFactory.php} | 8 +- .../Entity/Address/ProxyAddressFactory.php | 6 +- .../Address/ProxyCascadeAddressFactory.php | 35 -- .../Entity/Address/StandardAddressFactory.php | 35 -- ...ategoryFactory.php => CategoryFactory.php} | 8 +- .../Category/ProxyCascadeCategoryFactory.php | 35 -- .../Entity/Category/ProxyCategoryFactory.php | 6 +- .../Category/StandardCategoryFactory.php | 35 -- .../Entity/Contact/ChildContactFactory.php | 4 +- ...eContactFactory.php => ContactFactory.php} | 16 +- .../Contact/ProxyCascadeContactFactory.php | 39 -- .../Entity/Contact/ProxyContactFactory.php | 6 +- .../Entity/Contact/StandardContactFactory.php | 39 -- .../Entity/Tag/ProxyCascadeTagFactory.php | 35 -- .../Factories/Entity/Tag/ProxyTagFactory.php | 6 +- .../Entity/Tag/StandardTagFactory.php | 35 -- .../{CascadeTagFactory.php => TagFactory.php} | 8 +- .../WithHooksInInitializeFactory.php | 8 +- .../Maker/expected/can_create_factory.php | 12 +- .../can_create_factory_interactively.php | 15 +- ...ysis_annotations_with_data_set_phpstan.php | 70 ++-- ...alysis_annotations_with_data_set_psalm.php | 70 ++-- tests/Fixture/TestKernel.php | 8 + tests/Integration/Maker/MakeFactoryTest.php | 26 +- .../ResetDatabaseWithMigrationTest.php | 10 +- .../CascadeEntityFactoryRelationshipTest.php | 44 --- .../ORM/EdgeCasesRelationshipTest.php | 78 ++-- .../EntityFactoryRelationshipTestCase.php | 354 ++++++++++-------- ...lymorphicEntityFactoryRelationshipTest.php | 49 +++ .../ProxyEntityFactoryRelationshipTest.php} | 96 ++--- .../StandardEntityFactoryRelationshipTest.php | 40 ++ ...lymorphicEntityFactoryRelationshipTest.php | 26 -- ...xyCascadeEntityFactoryRelationshipTest.php | 44 --- .../ProxyEntityFactoryRelationshipTest.php | 44 --- .../StandardEntityFactoryRelationshipTest.php | 44 --- tests/Unit/FactoryTest.php | 18 +- 67 files changed, 897 insertions(+), 1396 deletions(-) create mode 100644 tests/Fixture/DoctrineCascadeRelationship/ChangeCascadePersistOnLoadClassMetadataListener.php create mode 100644 tests/Fixture/DoctrineCascadeRelationship/ChangesEntityRelationshipCascadePersist.php create mode 100644 tests/Fixture/DoctrineCascadeRelationship/DoctrineCascadeRelationshipMetadata.php create mode 100644 tests/Fixture/DoctrineCascadeRelationship/PhpUnitTestExtension.php create mode 100644 tests/Fixture/DoctrineCascadeRelationship/UsingRelationships.php delete mode 100644 tests/Fixture/Entity/Address/CascadeAddress.php delete mode 100644 tests/Fixture/Entity/Address/StandardAddress.php delete mode 100644 tests/Fixture/Entity/Category/CascadeCategory.php delete mode 100644 tests/Fixture/Entity/Category/StandardCategory.php rename tests/Fixture/Entity/{Contact => }/ChildContact.php (74%) delete mode 100644 tests/Fixture/Entity/Contact/CascadeContact.php delete mode 100644 tests/Fixture/Entity/Contact/StandardContact.php delete mode 100644 tests/Fixture/Entity/EdgeCases/RelationshipWithGlobalEntity/CascadeRelationshipWithGlobalEntity.php delete mode 100644 tests/Fixture/Entity/EdgeCases/RelationshipWithGlobalEntity/StandardRelationshipWithGlobalEntity.php delete mode 100644 tests/Fixture/Entity/EdgeCases/RichDomainMandatoryRelationship/CascadeInversedSideEntity.php delete mode 100644 tests/Fixture/Entity/EdgeCases/RichDomainMandatoryRelationship/CascadeOwningSideEntity.php rename tests/Fixture/Entity/EdgeCases/RichDomainMandatoryRelationship/{InversedSideEntity.php => InversedSide.php} (77%) rename tests/Fixture/Entity/EdgeCases/RichDomainMandatoryRelationship/{OwningSideEntity.php => OwningSide.php} (74%) delete mode 100644 tests/Fixture/Entity/EdgeCases/RichDomainMandatoryRelationship/StandardInversedSideEntity.php delete mode 100644 tests/Fixture/Entity/EdgeCases/RichDomainMandatoryRelationship/StandardOwningSideEntity.php delete mode 100644 tests/Fixture/Entity/Tag/CascadeTag.php delete mode 100644 tests/Fixture/Entity/Tag/StandardTag.php rename tests/Fixture/Factories/Entity/Address/{CascadeAddressFactory.php => AddressFactory.php} (73%) delete mode 100644 tests/Fixture/Factories/Entity/Address/ProxyCascadeAddressFactory.php delete mode 100644 tests/Fixture/Factories/Entity/Address/StandardAddressFactory.php rename tests/Fixture/Factories/Entity/Category/{CascadeCategoryFactory.php => CategoryFactory.php} (73%) delete mode 100644 tests/Fixture/Factories/Entity/Category/ProxyCascadeCategoryFactory.php delete mode 100644 tests/Fixture/Factories/Entity/Category/StandardCategoryFactory.php rename tests/Fixture/Factories/Entity/Contact/{CascadeContactFactory.php => ContactFactory.php} (60%) delete mode 100644 tests/Fixture/Factories/Entity/Contact/ProxyCascadeContactFactory.php delete mode 100644 tests/Fixture/Factories/Entity/Contact/StandardContactFactory.php delete mode 100644 tests/Fixture/Factories/Entity/Tag/ProxyCascadeTagFactory.php delete mode 100644 tests/Fixture/Factories/Entity/Tag/StandardTagFactory.php rename tests/Fixture/Factories/Entity/Tag/{CascadeTagFactory.php => TagFactory.php} (75%) delete mode 100644 tests/Integration/ORM/CascadeEntityFactoryRelationshipTest.php rename tests/Integration/ORM/{ => EntityRelationship}/EntityFactoryRelationshipTestCase.php (52%) create mode 100644 tests/Integration/ORM/EntityRelationship/PolymorphicEntityFactoryRelationshipTest.php rename tests/Integration/ORM/{ProxyEntityFactoryRelationshipTestCase.php => EntityRelationship/ProxyEntityFactoryRelationshipTest.php} (63%) create mode 100644 tests/Integration/ORM/EntityRelationship/StandardEntityFactoryRelationshipTest.php delete mode 100644 tests/Integration/ORM/PolymorphicEntityFactoryRelationshipTest.php delete mode 100644 tests/Integration/ORM/ProxyCascadeEntityFactoryRelationshipTest.php delete mode 100644 tests/Integration/ORM/ProxyEntityFactoryRelationshipTest.php delete mode 100644 tests/Integration/ORM/StandardEntityFactoryRelationshipTest.php diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 936e3c130..9c232ee52 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,7 +8,7 @@ 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.without-dama == 0 && contains(matrix.database, 'sql') && ' (dama)' || '' }}${{ matrix.use-phpunit-extension == 1 && ' (phpunit extension)' || '' }} runs-on: ubuntu-latest strategy: fail-fast: false @@ -16,34 +16,34 @@ jobs: php: [ 8.1, 8.2, 8.3, 8.4 ] symfony: [ 6.4.*, 7.1.*, 7.2.* ] database: [ mysql, mongo ] + phpunit: [ 9, 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.*} + - {php: 8.1, phpunit: 11 } 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.3, symfony: '*', phpunit: 9, database: none} + - {php: 8.3, symfony: '*', phpunit: 11, database: mysql|mongo} + - {php: 8.3, symfony: '*', phpunit: 11, database: pgsql|mongo} + - {php: 8.3, symfony: '*', phpunit: 11, database: pgsql, without-dama: 1} + - {php: 8.3, symfony: '*', phpunit: 11, database: sqlite, without-dama: 1} + - {php: 8.3, symfony: '*', phpunit: 9, database: sqlite, without-dama: 1, deps: lowest} + - {php: 8.3, symfony: '*', phpunit: 9, database: mysql, deps: lowest} + - {php: 8.3, symfony: '*', phpunit: 10, database: mysql|mongo} + - {php: 8.3, symfony: '*', phpunit: 11, database: mysql|mongo, use-phpunit-extension: 1} + - {php: 8.3, symfony: '*', phpunit: 11, database: mysql|mongo, use-phpunit-extension: 1, without-dama: 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_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' || '' }} diff --git a/bin/tools/phpstan/composer.lock b/bin/tools/phpstan/composer.lock index 153ae99e8..948dea282 100644 --- a/bin/tools/phpstan/composer.lock +++ b/bin/tools/phpstan/composer.lock @@ -34,13 +34,13 @@ }, "type": "phpstan-extension", "extra": { - "branch-alias": { - "dev-master": "1.0-dev" - }, "phpstan": { "includes": [ "extension.neon" ] + }, + "branch-alias": { + "dev-master": "1.0-dev" } }, "autoload": { @@ -117,16 +117,16 @@ }, { "name": "phpstan/phpstan", - "version": "1.12.7", + "version": "1.12.12", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "dc2b9976bd8b0f84ec9b0e50cc35378551de7af0" + "reference": "b5ae1b88f471d3fd4ba1aa0046234b5ca3776dd0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/dc2b9976bd8b0f84ec9b0e50cc35378551de7af0", - "reference": "dc2b9976bd8b0f84ec9b0e50cc35378551de7af0", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/b5ae1b88f471d3fd4ba1aa0046234b5ca3776dd0", + "reference": "b5ae1b88f471d3fd4ba1aa0046234b5ca3776dd0", "shasum": "" }, "require": { @@ -171,25 +171,25 @@ "type": "github" } ], - "time": "2024-10-18T11:12:07+00:00" + "time": "2024-11-28T22:13:23+00:00" }, { "name": "phpstan/phpstan-doctrine", - "version": "1.5.3", + "version": "1.5.7", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-doctrine.git", - "reference": "38db3bad8f1567d7bf64806738d724261f8a2b5c" + "reference": "231d3f795ed5ef54c98961fd3958868cbe091207" }, "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/231d3f795ed5ef54c98961fd3958868cbe091207", + "reference": "231d3f795ed5ef54c98961fd3958868cbe091207", "shasum": "" }, "require": { "php": "^7.2 || ^8.0", - "phpstan/phpstan": "^1.11.7" + "phpstan/phpstan": "^1.12.12" }, "conflict": { "doctrine/collections": "<1.0", @@ -216,7 +216,7 @@ "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", + "phpunit/phpunit": "^9.6.20", "ramsey/uuid": "^4.2", "symfony/cache": "^5.4" }, @@ -241,27 +241,27 @@ "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/1.5.7" }, - "time": "2024-09-01T13:17:34+00:00" + "time": "2024-12-02T16:47:26+00:00" }, { "name": "phpstan/phpstan-phpunit", - "version": "1.4.0", + "version": "1.4.1", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-phpunit.git", - "reference": "f3ea021866f4263f07ca3636bf22c64be9610c11" + "reference": "11d4235fbc6313ecbf93708606edfd3222e44949" }, "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/11d4235fbc6313ecbf93708606edfd3222e44949", + "reference": "11d4235fbc6313ecbf93708606edfd3222e44949", "shasum": "" }, "require": { "php": "^7.2 || ^8.0", - "phpstan/phpstan": "^1.11" + "phpstan/phpstan": "^1.12" }, "conflict": { "phpunit/phpunit": "<7.0" @@ -293,22 +293,22 @@ "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/1.4.1" }, - "time": "2024-04-20T06:39:00+00:00" + "time": "2024-11-12T12:43:59+00:00" }, { "name": "phpstan/phpstan-symfony", - "version": "1.4.10", + "version": "1.4.12", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-symfony.git", - "reference": "f7d5782044bedf93aeb3f38e09c91148ee90e5a1" + "reference": "c7b7e7f520893621558bfbfdb2694d4364565c1d" }, "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/c7b7e7f520893621558bfbfdb2694d4364565c1d", + "reference": "c7b7e7f520893621558bfbfdb2694d4364565c1d", "shasum": "" }, "require": { @@ -365,18 +365,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/1.4.12" }, - "time": "2024-09-26T18:14:50+00:00" + "time": "2024-11-06T10:13:18+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/phpunit b/phpunit index b62d8710b..01c97cf36 100755 --- a/phpunit +++ b/phpunit @@ -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-paratest.xml.dist b/phpunit-paratest.xml.dist index 66b9f9a74..387352e86 100644 --- a/phpunit-paratest.xml.dist +++ b/phpunit-paratest.xml.dist @@ -26,6 +26,7 @@ + diff --git a/tests/Fixture/DoctrineCascadeRelationship/ChangeCascadePersistOnLoadClassMetadataListener.php b/tests/Fixture/DoctrineCascadeRelationship/ChangeCascadePersistOnLoadClassMetadataListener.php new file mode 100644 index 000000000..cf76ff7ae --- /dev/null +++ b/tests/Fixture/DoctrineCascadeRelationship/ChangeCascadePersistOnLoadClassMetadataListener.php @@ -0,0 +1,41 @@ + + * + * 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'] : []; + } + } + } +} diff --git a/tests/Fixture/DoctrineCascadeRelationship/ChangesEntityRelationshipCascadePersist.php b/tests/Fixture/DoctrineCascadeRelationship/ChangesEntityRelationshipCascadePersist.php new file mode 100644 index 000000000..820c7a85f --- /dev/null +++ b/tests/Fixture/DoctrineCascadeRelationship/ChangesEntityRelationshipCascadePersist.php @@ -0,0 +1,119 @@ + + * + * 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 (count($usingRelationshipsAttributes) !== 1 || $usingRelationshipsAttributes[0]->newInstance()->methodName() !== 'provideCascadeRelationshipsCombinations') { + 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($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']]; + if ($association['inversedBy'] ?? $association['mappedBy'] ?? null) { + $relationshipFields[] = ['class' => $association['targetEntity'], 'field' => $association['inversedBy'] ?? $association['mappedBy']]; + } + } + } + + 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..e860a3f14 --- /dev/null +++ b/tests/Fixture/DoctrineCascadeRelationship/DoctrineCascadeRelationshipMetadata.php @@ -0,0 +1,63 @@ + + */ +final class DoctrineCascadeRelationshipMetadata implements \Stringable +{ + private function __construct( + public readonly string $class, + public readonly string $field, + public readonly bool $cascade, + ) { + } + + /** + * @param array{class: class-string, field: string} $source + */ + public static function fromArray(array $source, bool $cascade = false): self + { + return new self(class: $source['class'], field: $source['field'], cascade: $cascade); + } + + public function __toString(): string + { + return \sprintf('%s::$%s - %s', $this->class, $this->field, $this->cascade ? 'cascade' : 'no cascade'); + } + + /** + * @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 = DoctrineCascadeRelationshipMetadata::fromArray($relationshipFields[0]); + + yield "{$metadata}\n" => [$metadata]; + + return; + } + + $total = pow(2, count($relationshipFields)); + + for ($i = 0; $i < $total; $i++) { + $temp = []; + + $permutationName = "\n"; + for ($j = 0; $j < count($relationshipFields); $j++) { + $metadata = DoctrineCascadeRelationshipMetadata::fromArray($relationshipFields[$j], cascade: (bool)(($i >> $j) & 1)); + + $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..5df77dc7b --- /dev/null +++ b/tests/Fixture/DoctrineCascadeRelationship/PhpUnitTestExtension.php @@ -0,0 +1,40 @@ + + */ +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..71306a616 --- /dev/null +++ b/tests/Fixture/DoctrineCascadeRelationship/UsingRelationships.php @@ -0,0 +1,20 @@ + + */ +#[\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/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/RelationshipWithGlobalEntity/CascadeRelationshipWithGlobalEntity.php b/tests/Fixture/Entity/EdgeCases/RelationshipWithGlobalEntity/CascadeRelationshipWithGlobalEntity.php deleted file mode 100644 index 233333f6c..000000000 --- a/tests/Fixture/Entity/EdgeCases/RelationshipWithGlobalEntity/CascadeRelationshipWithGlobalEntity.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\EdgeCases\RelationshipWithGlobalEntity; - -use Doctrine\ORM\Mapping as ORM; -use Zenstruck\Foundry\Tests\Fixture\Entity\GlobalEntity; - -/** - * @author Nicolas PHILIPPE - */ -#[ORM\Entity] -class CascadeRelationshipWithGlobalEntity extends RelationshipWithGlobalEntity -{ - #[ORM\ManyToOne(targetEntity: GlobalEntity::class, cascade: ['persist'])] - protected ?GlobalEntity $globalEntity = null; -} 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/RelationshipWithGlobalEntity/StandardRelationshipWithGlobalEntity.php b/tests/Fixture/Entity/EdgeCases/RelationshipWithGlobalEntity/StandardRelationshipWithGlobalEntity.php deleted file mode 100644 index 180a4b19f..000000000 --- a/tests/Fixture/Entity/EdgeCases/RelationshipWithGlobalEntity/StandardRelationshipWithGlobalEntity.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\EdgeCases\RelationshipWithGlobalEntity; - -use Doctrine\ORM\Mapping as ORM; -use Zenstruck\Foundry\Tests\Fixture\Entity\GlobalEntity; - -/** - * @author Nicolas PHILIPPE - */ -#[ORM\Entity] -class StandardRelationshipWithGlobalEntity extends RelationshipWithGlobalEntity -{ - #[ORM\ManyToOne(targetEntity: GlobalEntity::class)] - protected ?GlobalEntity $globalEntity = null; -} 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 74% rename from tests/Fixture/Entity/EdgeCases/RichDomainMandatoryRelationship/OwningSideEntity.php rename to tests/Fixture/Entity/EdgeCases/RichDomainMandatoryRelationship/OwningSide.php index e5d8a314c..6001e617c 100644 --- a/tests/Fixture/Entity/EdgeCases/RichDomainMandatoryRelationship/OwningSideEntity.php +++ b/tests/Fixture/Entity/EdgeCases/RichDomainMandatoryRelationship/OwningSide.php @@ -16,17 +16,18 @@ 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')] + 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/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/Factories/Entity/Address/CascadeAddressFactory.php b/tests/Fixture/Factories/Entity/Address/AddressFactory.php similarity index 73% rename from tests/Fixture/Factories/Entity/Address/CascadeAddressFactory.php rename to tests/Fixture/Factories/Entity/Address/AddressFactory.php index 25b362693..b7fa6013d 100644 --- a/tests/Fixture/Factories/Entity/Address/CascadeAddressFactory.php +++ b/tests/Fixture/Factories/Entity/Address/AddressFactory.php @@ -12,18 +12,18 @@ 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 diff --git a/tests/Fixture/Factories/Entity/Address/ProxyAddressFactory.php b/tests/Fixture/Factories/Entity/Address/ProxyAddressFactory.php index c3dca085a..f61838bf0 100644 --- a/tests/Fixture/Factories/Entity/Address/ProxyAddressFactory.php +++ b/tests/Fixture/Factories/Entity/Address/ProxyAddressFactory.php @@ -12,18 +12,18 @@ 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 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 73% rename from tests/Fixture/Factories/Entity/Category/CascadeCategoryFactory.php rename to tests/Fixture/Factories/Entity/Category/CategoryFactory.php index c50d705d3..e589575dc 100644 --- a/tests/Fixture/Factories/Entity/Category/CascadeCategoryFactory.php +++ b/tests/Fixture/Factories/Entity/Category/CategoryFactory.php @@ -12,18 +12,18 @@ 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 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..4a7793cb7 100644 --- a/tests/Fixture/Factories/Entity/Category/ProxyCategoryFactory.php +++ b/tests/Fixture/Factories/Entity/Category/ProxyCategoryFactory.php @@ -12,18 +12,18 @@ 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 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 60% rename from tests/Fixture/Factories/Entity/Contact/CascadeContactFactory.php rename to tests/Fixture/Factories/Entity/Contact/ContactFactory.php index e92ac9517..3e06da2ad 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 { 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..af45e5d44 100644 --- a/tests/Fixture/Factories/Entity/Contact/ProxyContactFactory.php +++ b/tests/Fixture/Factories/Entity/Contact/ProxyContactFactory.php @@ -12,20 +12,20 @@ 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 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/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..5e94d3105 100644 --- a/tests/Fixture/Factories/Entity/Tag/ProxyTagFactory.php +++ b/tests/Fixture/Factories/Entity/Tag/ProxyTagFactory.php @@ -12,18 +12,18 @@ 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 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 75% rename from tests/Fixture/Factories/Entity/Tag/CascadeTagFactory.php rename to tests/Fixture/Factories/Entity/Tag/TagFactory.php index 5b4586454..9a088a00f 100644 --- a/tests/Fixture/Factories/Entity/Tag/CascadeTagFactory.php +++ b/tests/Fixture/Factories/Entity/Tag/TagFactory.php @@ -12,18 +12,18 @@ 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 diff --git a/tests/Fixture/Factories/WithHooksInInitializeFactory.php b/tests/Fixture/Factories/WithHooksInInitializeFactory.php index faa389f1f..a71744e47 100644 --- a/tests/Fixture/Factories/WithHooksInInitializeFactory.php +++ b/tests/Fixture/Factories/WithHooksInInitializeFactory.php @@ -14,17 +14,17 @@ namespace Zenstruck\Foundry\Tests\Fixture\Factories; use Zenstruck\Foundry\Persistence\PersistentObjectFactory; -use Zenstruck\Foundry\Tests\Fixture\Entity\Address\StandardAddress; +use Zenstruck\Foundry\Tests\Fixture\Entity\Address; /** * @author Nicolas Philippe - * @extends PersistentObjectFactory + * @extends PersistentObjectFactory
*/ final class WithHooksInInitializeFactory extends PersistentObjectFactory { public static function class(): string { - return StandardAddress::class; + return Address::class; } protected function defaults(): array|callable @@ -47,7 +47,7 @@ function(array $parameters, string $class, WithHooksInInitializeFactory $factory } ) ->afterInstantiate( - function(StandardAddress $object, array $parameters, WithHooksInInitializeFactory $factory) { + function(Address $object, array $parameters, WithHooksInInitializeFactory $factory) { if (!$factory->isPersisting()) { $object->setCity("{$object->getCity()} - afterInstantiate"); } 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_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..eb57e2f05 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..6c1020360 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/TestKernel.php b/tests/Fixture/TestKernel.php index 182f63e6d..50ae9529c 100644 --- a/tests/Fixture/TestKernel.php +++ b/tests/Fixture/TestKernel.php @@ -23,6 +23,8 @@ use Symfony\Component\HttpKernel\Kernel; use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; use Zenstruck\Foundry\ORM\ResetDatabase\ResetDatabaseMode; +use Zenstruck\Foundry\Persistence\PersistenceManager; +use Zenstruck\Foundry\Tests\Fixture\DoctrineCascadeRelationship\ChangeCascadePersistOnLoadClassMetadataListener; use Zenstruck\Foundry\Tests\Fixture\Factories\ArrayFactory; use Zenstruck\Foundry\Tests\Fixture\Factories\Object1Factory; use Zenstruck\Foundry\Tests\Fixture\Stories\GlobalInvokableService; @@ -118,6 +120,12 @@ protected function configureContainer(ContainerBuilder $c, LoaderInterface $load 'controller_resolver' => ['auto_mapping' => false], ], ]); + + $c->register(ChangeCascadePersistOnLoadClassMetadataListener::class) + ->setAutowired(true) + ->setAutoconfigured(true); + $c->setAlias(PersistenceManager::class, '.zenstruck_foundry.persistence_manager') + ->setPublic(true); } if (\getenv('MONGO_URL')) { 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 index c4e5cfc02..eb07f3cc7 100644 --- a/tests/Integration/Migration/ResetDatabaseWithMigrationTest.php +++ b/tests/Integration/Migration/ResetDatabaseWithMigrationTest.php @@ -21,7 +21,7 @@ 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\Factories\Entity\Contact\ContactFactory; use Zenstruck\Foundry\Tests\Fixture\MigrationTests\TestMigrationKernel; use Zenstruck\Foundry\Tests\Integration\RequiresORM; @@ -62,11 +62,11 @@ public function it_generates_valid_schema(): void */ public function it_can_store_object(): void { - StandardContactFactory::assert()->count(0); + ContactFactory::assert()->count(0); - StandardContactFactory::createOne(); + ContactFactory::createOne(); - StandardContactFactory::assert()->count(1); + ContactFactory::assert()->count(1); } /** @@ -75,7 +75,7 @@ public function it_can_store_object(): void */ public function it_starts_from_fresh_db(): void { - StandardContactFactory::assert()->count(0); + ContactFactory::assert()->count(0); } /** diff --git a/tests/Integration/ORM/CascadeEntityFactoryRelationshipTest.php b/tests/Integration/ORM/CascadeEntityFactoryRelationshipTest.php deleted file mode 100644 index 8a2822d67..000000000 --- a/tests/Integration/ORM/CascadeEntityFactoryRelationshipTest.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\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 -{ - 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..e77f9d95f 100644 --- a/tests/Integration/ORM/EdgeCasesRelationshipTest.php +++ b/tests/Integration/ORM/EdgeCasesRelationshipTest.php @@ -13,11 +13,17 @@ 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\Contact; use Zenstruck\Foundry\Tests\Fixture\Entity\EdgeCases\InversedOneToOneWithNonNullableOwning; use Zenstruck\Foundry\Tests\Fixture\Entity\EdgeCases\ManyToOneToSelfReferencing; use Zenstruck\Foundry\Tests\Fixture\Entity\EdgeCases\RelationshipWithGlobalEntity; @@ -36,49 +42,45 @@ */ 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 + use Factories, ChangesEntityRelationshipCascadePersist, RequiresORM, ResetDatabase; + + /** @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, $asProxy) { - $globalEntity = $asProxy ? GlobalStory::globalEntityProxy() : GlobalStory::globalEntity(); - self::assertSame($asProxy, $globalEntity instanceof Proxy); - - $relationshipWithGlobalEntityFactory->create(['globalEntity' => $globalEntity]); + 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(1); + $relationshipWithGlobalEntityFactory::assert()->count(2); $entity = $relationshipWithGlobalEntityFactory::repository()->first(); self::assertSame(GlobalStory::globalEntity(), $entity?->getGlobalEntity()); - } - public static function relationshipWithGlobalEntityFactoryProvider(): iterable - { - 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]; + $entity = $relationshipWithGlobalEntityFactory::repository()->last(); + self::assertSame(GlobalStory::globalEntity(), $entity?->getGlobalEntity()); } - /** - * @test - * @param PersistentObjectFactory $inversedSideEntityFactory - * @param PersistentObjectFactory $owningSideEntityFactory - * @dataProvider richDomainMandatoryRelationshipFactoryProvider - */ - public function inversed_relationship_mandatory(PersistentObjectFactory $inversedSideEntityFactory, PersistentObjectFactory $owningSideEntityFactory): void + /** @test */ + #[Test] + #[DataProvider('provideCascadeRelationshipsCombinations')] + #[UsingRelationships(RichDomainMandatoryRelationship\OwningSide::class, ['main'])] + #[RequiresPhpunit('11.4')] + public function inversed_relationship_mandatory(): void { + $owningSideEntityFactory = persistent_factory(RichDomainMandatoryRelationship\OwningSide::class); + $inversedSideEntityFactory = persistent_factory(RichDomainMandatoryRelationship\InversedSide::class); + $inversedSideEntity = $inversedSideEntityFactory->create([ 'relations' => $owningSideEntityFactory->many(2), ]); @@ -88,26 +90,6 @@ 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 */ diff --git a/tests/Integration/ORM/EntityFactoryRelationshipTestCase.php b/tests/Integration/ORM/EntityRelationship/EntityFactoryRelationshipTestCase.php similarity index 52% rename from tests/Integration/ORM/EntityFactoryRelationshipTestCase.php rename to tests/Integration/ORM/EntityRelationship/EntityFactoryRelationshipTestCase.php index 1ccea746e..59f90225a 100644 --- a/tests/Integration/ORM/EntityFactoryRelationshipTestCase.php +++ b/tests/Integration/ORM/EntityRelationship/EntityFactoryRelationshipTestCase.php @@ -1,17 +1,13 @@ - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ +declare(strict_types=1); -namespace Zenstruck\Foundry\Tests\Integration\ORM; +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; @@ -19,89 +15,81 @@ use Zenstruck\Foundry\Persistence\PersistentObjectFactory; use Zenstruck\Foundry\Test\Factories; use Zenstruck\Foundry\Test\ResetDatabase; +use Zenstruck\Foundry\Tests\Fixture\DoctrineCascadeRelationship\UsingRelationships; +use Zenstruck\Foundry\Tests\Fixture\DoctrineCascadeRelationship\ChangesEntityRelationshipCascadePersist; 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 + * @author Nicolas PHILIPPE + * @requires PHPUnit ^11.4 */ +#[RequiresPhpunit('^11.4')] abstract class EntityFactoryRelationshipTestCase extends KernelTestCase { - use Factories, RequiresORM, ResetDatabase; + use Factories, ChangesEntityRelationshipCascadePersist, ResetDatabase; - /** - * @test - */ + /** @test */ + #[Test] + #[DataProvider('provideCascadeRelationshipsCombinations')] + #[UsingRelationships(Contact::class, ['category'])] public function many_to_one(): void { - $contact = static::contactFactory()::createOne([ + $contact = static::contactFactory()->create([ 'category' => static::categoryFactory(), ]); - static::contactFactory()::repository()->assert()->count(1); - static::categoryFactory()::repository()->assert()->count(1); + static::contactFactory()::assert()->count(1); + static::categoryFactory()::assert()->count(1); $this->assertNotNull($contact->id); $this->assertNotNull($contact->getCategory()?->id); } - /** - * @test - */ - public function disabling_persistence_cascades_to_children(): void + /** @test */ + #[Test] + #[DataProvider('provideCascadeRelationshipsCombinations')] + #[UsingRelationships(Category::class, ['contacts'])] + public function one_to_many_with_factory_collection(): 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->one_to_many(static::contactFactory()->many(2)); + } - $this->assertNull($category->id); - $this->assertCount(3, $category->getContacts()); + /** @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()]); + } - foreach ($category->getContacts() as $contact) { - $this->assertSame($category->getName(), $contact->getCategory()?->getName()); - } + /** @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 - * @param FactoryCollection>|list> $contacts - * @dataProvider one_to_many_provider + * @param FactoryCollection>|list>|list $contacts */ - public function one_to_many(FactoryCollection|array $contacts): void + private function one_to_many(FactoryCollection|array $contacts): void { - $category = static::categoryFactory()::createOne([ + $category = static::categoryFactory()->create([ 'contacts' => $contacts, ]); - static::contactFactory()::repository()->assert()->count(2); - static::categoryFactory()::repository()->assert()->count(1); + static::contactFactory()::assert()->count(2); + static::categoryFactory()::assert()->count(1); + $this->assertNotNull($category->id); $this->assertCount(2, $category->getContacts()); @@ -110,24 +98,17 @@ public function one_to_many(FactoryCollection|array $contacts): void } } - 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 - */ + /** @test */ + #[Test] + #[DataProvider('provideCascadeRelationshipsCombinations')] + #[UsingRelationships(Category::class, ['contacts'])] + #[UsingRelationships(Contact::class, ['address'])] 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::contactFactoryWithoutCategory(), + static::contactFactoryWithoutCategory()->create(), ], ]); @@ -139,34 +120,34 @@ public function inverse_one_to_many_relationship(): void } } - /** - * @test - */ + /** @test */ + #[Test] + #[DataProvider('provideCascadeRelationshipsCombinations')] + #[UsingRelationships(Tag::class, ['contacts'])] 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); + $this->many_to_many(static::contactFactory()->many(3)); + } - foreach ($tag->getContacts() as $contact) { - $this->assertSame($tag->id, $contact->getTags()[0]?->id); - } + /** @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 + * @param FactoryCollection>|list>|list $contacts */ - public function many_to_many_owning_as_array(): void + private function many_to_many(FactoryCollection|array $contacts): void { - $tag = static::tagFactory()::createOne([ - 'contacts' => [static::contactFactory(), static::contactFactory(), static::contactFactory()], + $tag = static::tagFactory()->create([ + 'contacts' => $contacts, ]); - static::contactFactory()::repository()->assert()->count(3); + static::contactFactory()::assert()->count(3); static::tagFactory()::repository()->assert()->count(1); $this->assertNotNull($tag->id); @@ -175,17 +156,19 @@ public function many_to_many_owning_as_array(): void } } - /** - * @test - */ + /** @test */ + #[Test] + #[DataProvider('provideCascadeRelationshipsCombinations')] + #[UsingRelationships(Contact::class, ['tags'])] public function many_to_many_inverse(): void { - $contact = static::contactFactory()::createOne([ - 'tags' => static::tagFactory()->many(3), + $contact = static::contactFactory()->create([ + 'tags' => static::tagFactory()::new()->many(3), ]); - static::contactFactory()::repository()->assert()->count(1); - static::tagFactory()::repository()->assert()->count(3); + static::contactFactory()::assert()->count(1); + static::tagFactory()::assert()->count(3); + $this->assertNotNull($contact->id); foreach ($contact->getTags() as $tag) { @@ -194,23 +177,40 @@ public function many_to_many_inverse(): void } } - /** - * @test - */ + /** @test */ + #[Test] + #[DataProvider('provideCascadeRelationshipsCombinations')] + #[UsingRelationships(Contact::class, ['address'])] public function one_to_one_owning(): void { - $contact = static::contactFactory()::createOne(); + $contact = static::contactFactory()->create(); - static::contactFactory()::repository()->assert()->count(1); - static::addressFactory()::repository()->assert()->count(1); + static::contactFactory()::assert()->count(1); + static::addressFactory()::assert()->count(1); $this->assertNotNull($contact->id); $this->assertNotNull($contact->getAddress()->id); } - /** - * @test - */ + /** @test */ + #[Test] + #[DataProvider('provideCascadeRelationshipsCombinations')] + #[UsingRelationships(Address::class, ['contact'])] + #[UsingRelationships(Contact::class, ['address'])] + 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'])); @@ -224,20 +224,22 @@ public function many_to_one_unmanaged_raw_entity(): void $this->assertSame('Some city', $contact->getAddress()->getCity()); } - /** - * @test - */ + /** @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), - 'secondaryContacts' => static::contactFactory() - ->with(['category' => null]) // ensure no "main category" is set for secondary contacts - ->many(3), + + // 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); @@ -250,59 +252,48 @@ public function one_to_many_with_two_relationships_same_entity(): void } } - /** - * @test - */ + /** @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::contactFactory()->with(['category' => null]), - 'addSecondaryContact' => static::contactFactory()->with(['category' => null]), + '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 */ + #[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(3), + 'secondaryContacts' => static::contactFactory()->many(2), ]); $this->assertCount(3, $tag->getContacts()); - $this->assertCount(3, $tag->getSecondaryContacts()); - static::tagFactory()::assert()->count(1); - static::contactFactory()::assert()->count(6); - } + $this->assertCount(2, $tag->getSecondaryContacts()); - /** - * @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); + static::contactFactory()::assert()->count(5); + static::tagFactory()::assert()->count(1); } - /** - * @test - */ + /** @test */ + #[Test] + #[DataProvider('provideCascadeRelationshipsCombinations')] + #[UsingRelationships(Category::class, ['contacts', 'secondaryContacts'])] public function can_use_adder_as_attributes(): void { $category = static::categoryFactory()->create([ @@ -313,9 +304,10 @@ public function can_use_adder_as_attributes(): void self::assertSame('foo', $category->getContacts()[0]?->getName()); } - /** - * @test - */ + /** @test */ + #[Test] + #[DataProvider('provideCascadeRelationshipsCombinations')] + #[UsingRelationships(Category::class, ['contacts'])] public function forced_one_to_many_with_doctrine_collection_type(): void { $category = static::categoryFactory() @@ -333,10 +325,52 @@ public function forced_one_to_many_with_doctrine_collection_type(): void static::categoryFactory()::assert()->count(1); } - /** - * @test - */ - public function ensure_one_to_many_cascade_relations_are_not_pre_persisted(): void + /** @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() { @@ -356,23 +390,21 @@ public function ensure_one_to_many_cascade_relations_are_not_pre_persisted(): vo } } - /** - * @return PersistentObjectFactory - */ + /** @return PersistentObjectFactory */ + protected static function contactFactoryWithoutCategory(): PersistentObjectFactory + { + return static::contactFactory()->with(['category' => null]); + } + + /** @return PersistentObjectFactory */ abstract protected static function contactFactory(): PersistentObjectFactory; - /** - * @return PersistentObjectFactory - */ + /** @return PersistentObjectFactory */ abstract protected static function categoryFactory(): PersistentObjectFactory; - /** - * @return PersistentObjectFactory - */ + /** @return PersistentObjectFactory */ abstract protected static function tagFactory(): PersistentObjectFactory; - /** - * @return PersistentObjectFactory
- */ + /** @return PersistentObjectFactory
*/ abstract protected static function addressFactory(): PersistentObjectFactory; } diff --git a/tests/Integration/ORM/EntityRelationship/PolymorphicEntityFactoryRelationshipTest.php b/tests/Integration/ORM/EntityRelationship/PolymorphicEntityFactoryRelationshipTest.php new file mode 100644 index 000000000..0eea999c5 --- /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 63% rename from tests/Integration/ORM/ProxyEntityFactoryRelationshipTestCase.php rename to tests/Integration/ORM/EntityRelationship/ProxyEntityFactoryRelationshipTest.php index d9217bbe8..9b20ef488 100644 --- a/tests/Integration/ORM/ProxyEntityFactoryRelationshipTestCase.php +++ b/tests/Integration/ORM/EntityRelationship/ProxyEntityFactoryRelationshipTest.php @@ -1,43 +1,37 @@ - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ +declare(strict_types=1); -namespace Zenstruck\Foundry\Tests\Integration\ORM; +namespace Zenstruck\Foundry\Tests\Integration\ORM\EntityRelationship; use Doctrine\ORM\EntityManagerInterface; use Doctrine\Persistence\Proxy as DoctrineProxy; use PHPUnit\Framework\AssertionFailedError; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\RequiresPhpunit; +use PHPUnit\Framework\Attributes\Test; use Zenstruck\Assert; use Zenstruck\Foundry\Persistence\Exception\RefreshObjectFailed; -use Zenstruck\Foundry\Persistence\PersistentProxyObjectFactory; use Zenstruck\Foundry\Persistence\Proxy; -use Zenstruck\Foundry\Tests\Fixture\Entity\Address; -use Zenstruck\Foundry\Tests\Fixture\Entity\Category; +use Zenstruck\Foundry\Tests\Fixture\DoctrineCascadeRelationship\UsingRelationships; use Zenstruck\Foundry\Tests\Fixture\Entity\Contact; -use Zenstruck\Foundry\Tests\Fixture\Entity\Tag; +use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\Address\ProxyAddressFactory; +use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\Category\ProxyCategoryFactory; +use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\Contact\ProxyContactFactory; +use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\Tag\ProxyTagFactory; /** + * @author Kevin Bond * @author Nicolas PHILIPPE - * - * @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()]); @@ -57,9 +51,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 +66,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 +82,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 +93,8 @@ public function can_assert_persisted(): void ; } - /** - * @test - */ + /** @test */ + #[Test] public function can_assert_not_persisted(): void { static::contactFactory()->withoutPersisting()->create()->_assertNotPersisted(); @@ -110,9 +104,8 @@ public function can_assert_not_persisted(): void ; } - /** - * @test - */ + /** @test */ + #[Test] public function can_remove_and_assert_not_persisted(): void { static::contactFactory() @@ -123,9 +116,8 @@ public function can_remove_and_assert_not_persisted(): void ; } - /** - * @test - */ + /** @test */ + #[Test] public function cannot_use_assert_persisted_when_entity_has_changes(): void { $contact = static::contactFactory()->create(); @@ -134,4 +126,24 @@ public function cannot_use_assert_persisted_when_entity_has_changes(): void $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..b2c7c91f7 --- /dev/null +++ b/tests/Integration/ORM/EntityRelationship/StandardEntityFactoryRelationshipTest.php @@ -0,0 +1,40 @@ + + * @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/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/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/Unit/FactoryTest.php b/tests/Unit/FactoryTest.php index 47141ec99..842a4c288 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\ProxyContactFactory; -use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\Contact\StandardContactFactory; +use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\Contact\ContactFactory; use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\GenericProxyEntityFactory; use Zenstruck\Foundry\Tests\Fixture\Object1; @@ -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()); } } From e667493281f5afcd16c56a662c9e2eeb1ec5fdc1 Mon Sep 17 00:00:00 2001 From: nikophil Date: Sat, 14 Dec 2024 21:20:35 +0000 Subject: [PATCH 048/102] bot: fix cs [skip ci] --- ...cadePersistOnLoadClassMetadataListener.php | 9 ++ ...hangesEntityRelationshipCascadePersist.php | 18 +++- .../PhpUnitTestExtension.php | 11 ++- .../ORM/EdgeCasesRelationshipTest.php | 6 +- .../EntityFactoryRelationshipTestCase.php | 89 ++++++++++--------- .../ProxyEntityFactoryRelationshipTest.php | 9 ++ .../StandardEntityFactoryRelationshipTest.php | 9 ++ tests/Unit/FactoryTest.php | 2 +- 8 files changed, 102 insertions(+), 51 deletions(-) diff --git a/tests/Fixture/DoctrineCascadeRelationship/ChangeCascadePersistOnLoadClassMetadataListener.php b/tests/Fixture/DoctrineCascadeRelationship/ChangeCascadePersistOnLoadClassMetadataListener.php index cf76ff7ae..e5b2e302b 100644 --- a/tests/Fixture/DoctrineCascadeRelationship/ChangeCascadePersistOnLoadClassMetadataListener.php +++ b/tests/Fixture/DoctrineCascadeRelationship/ChangeCascadePersistOnLoadClassMetadataListener.php @@ -2,6 +2,15 @@ declare(strict_types=1); +/* + * This file is part of the zenstruck/foundry package. + * + * (c) Kevin Bond + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Zenstruck\Foundry\Tests\Fixture\DoctrineCascadeRelationship; use Doctrine\Bundle\DoctrineBundle\Attribute\AsDoctrineListener; diff --git a/tests/Fixture/DoctrineCascadeRelationship/ChangesEntityRelationshipCascadePersist.php b/tests/Fixture/DoctrineCascadeRelationship/ChangesEntityRelationshipCascadePersist.php index 820c7a85f..1577ce343 100644 --- a/tests/Fixture/DoctrineCascadeRelationship/ChangesEntityRelationshipCascadePersist.php +++ b/tests/Fixture/DoctrineCascadeRelationship/ChangesEntityRelationshipCascadePersist.php @@ -2,6 +2,15 @@ declare(strict_types=1); +/* + * This file is part of the zenstruck/foundry package. + * + * (c) Kevin Bond + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Zenstruck\Foundry\Tests\Fixture\DoctrineCascadeRelationship; use Doctrine\ORM\Mapping\ClassMetadata; @@ -45,8 +54,8 @@ public function setUpCascadePersistMetadata(): void } $usingRelationshipsAttributes = $testMethod->getAttributes(DataProvider::class); - if (count($usingRelationshipsAttributes) !== 1 || $usingRelationshipsAttributes[0]->newInstance()->methodName() !== 'provideCascadeRelationshipsCombinations') { - throw new \LogicException(sprintf('When using attribute "%s", you must use "provideCascadeRelationshipsCombinations" as unique a data provider.', UsingRelationships::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 */ @@ -68,6 +77,7 @@ public static function provideCascadeRelationshipsCombinations(): iterable // 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; } @@ -92,14 +102,14 @@ public static function provideCascadeRelationshipsCombinations(): iterable $metadata = $persistenceManager->metadataFor($class); if (!$metadata instanceof ClassMetadata || $metadata->isEmbeddedClass) { - throw new \InvalidArgumentException("$class is not an entity using ORM"); + 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)); + throw new \LogicException(\sprintf("Wrong parameters for attribute \"%s\". Association \"{$class}::\${$field}\" does not exist.", UsingRelationships::class)); } $relationshipFields[] = ['class' => $association['sourceEntity'], 'field' => $association['fieldName']]; diff --git a/tests/Fixture/DoctrineCascadeRelationship/PhpUnitTestExtension.php b/tests/Fixture/DoctrineCascadeRelationship/PhpUnitTestExtension.php index 5df77dc7b..787c407e6 100644 --- a/tests/Fixture/DoctrineCascadeRelationship/PhpUnitTestExtension.php +++ b/tests/Fixture/DoctrineCascadeRelationship/PhpUnitTestExtension.php @@ -2,6 +2,15 @@ declare(strict_types=1); +/* + * This file is part of the zenstruck/foundry package. + * + * (c) Kevin Bond + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Zenstruck\Foundry\Tests\Fixture\DoctrineCascadeRelationship; use PHPUnit\Event; @@ -31,7 +40,7 @@ public function notify(Event\Test\DataProviderMethodCalled $event): void return; } - if (!method_exists($testMethod->className(), 'setCurrentProvidedMethodName')) { + if (!\method_exists($testMethod->className(), 'setCurrentProvidedMethodName')) { throw new \LogicException("Test \"{$testMethod->className()}::{$testMethod->methodName()}()\" should use trait ChangesEntityRelationshipCascadePersist."); } diff --git a/tests/Integration/ORM/EdgeCasesRelationshipTest.php b/tests/Integration/ORM/EdgeCasesRelationshipTest.php index e77f9d95f..9a0739fc6 100644 --- a/tests/Integration/ORM/EdgeCasesRelationshipTest.php +++ b/tests/Integration/ORM/EdgeCasesRelationshipTest.php @@ -17,13 +17,10 @@ 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\Contact; use Zenstruck\Foundry\Tests\Fixture\Entity\EdgeCases\InversedOneToOneWithNonNullableOwning; use Zenstruck\Foundry\Tests\Fixture\Entity\EdgeCases\ManyToOneToSelfReferencing; use Zenstruck\Foundry\Tests\Fixture\Entity\EdgeCases\RelationshipWithGlobalEntity; @@ -35,14 +32,13 @@ 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, ChangesEntityRelationshipCascadePersist, RequiresORM, ResetDatabase; + use ChangesEntityRelationshipCascadePersist, Factories, RequiresORM, ResetDatabase; /** @test */ #[Test] diff --git a/tests/Integration/ORM/EntityRelationship/EntityFactoryRelationshipTestCase.php b/tests/Integration/ORM/EntityRelationship/EntityFactoryRelationshipTestCase.php index 59f90225a..4ee765d23 100644 --- a/tests/Integration/ORM/EntityRelationship/EntityFactoryRelationshipTestCase.php +++ b/tests/Integration/ORM/EntityRelationship/EntityFactoryRelationshipTestCase.php @@ -2,6 +2,15 @@ declare(strict_types=1); +/* + * This file is part of the zenstruck/foundry package. + * + * (c) Kevin Bond + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Zenstruck\Foundry\Tests\Integration\ORM\EntityRelationship; use Doctrine\ORM\EntityManagerInterface; @@ -15,8 +24,8 @@ use Zenstruck\Foundry\Persistence\PersistentObjectFactory; use Zenstruck\Foundry\Test\Factories; use Zenstruck\Foundry\Test\ResetDatabase; -use Zenstruck\Foundry\Tests\Fixture\DoctrineCascadeRelationship\UsingRelationships; 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; @@ -32,7 +41,7 @@ #[RequiresPhpunit('^11.4')] abstract class EntityFactoryRelationshipTestCase extends KernelTestCase { - use Factories, ChangesEntityRelationshipCascadePersist, ResetDatabase; + use ChangesEntityRelationshipCascadePersist, Factories, ResetDatabase; /** @test */ #[Test] @@ -78,26 +87,6 @@ public function one_to_many_with_array_of_managed_objects(): void $this->one_to_many([static::contactFactoryWithoutCategory()->create(), static::contactFactoryWithoutCategory()->create()]); } - /** - * @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); - } - } - /** @test */ #[Test] #[DataProvider('provideCascadeRelationshipsCombinations')] @@ -138,24 +127,6 @@ public function many_to_many_owning_as_array(): void $this->many_to_many([static::contactFactory(), static::contactFactory(), static::contactFactory()]); } - /** - * @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); - } - } - /** @test */ #[Test] #[DataProvider('provideCascadeRelationshipsCombinations')] @@ -407,4 +378,42 @@ abstract protected static function tagFactory(): PersistentObjectFactory; /** @return PersistentObjectFactory
*/ abstract protected static function addressFactory(): PersistentObjectFactory; + + /** + * @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/ProxyEntityFactoryRelationshipTest.php b/tests/Integration/ORM/EntityRelationship/ProxyEntityFactoryRelationshipTest.php index 9b20ef488..ccd2a631c 100644 --- a/tests/Integration/ORM/EntityRelationship/ProxyEntityFactoryRelationshipTest.php +++ b/tests/Integration/ORM/EntityRelationship/ProxyEntityFactoryRelationshipTest.php @@ -2,6 +2,15 @@ declare(strict_types=1); +/* + * This file is part of the zenstruck/foundry package. + * + * (c) Kevin Bond + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Zenstruck\Foundry\Tests\Integration\ORM\EntityRelationship; use Doctrine\ORM\EntityManagerInterface; diff --git a/tests/Integration/ORM/EntityRelationship/StandardEntityFactoryRelationshipTest.php b/tests/Integration/ORM/EntityRelationship/StandardEntityFactoryRelationshipTest.php index b2c7c91f7..20095b9f5 100644 --- a/tests/Integration/ORM/EntityRelationship/StandardEntityFactoryRelationshipTest.php +++ b/tests/Integration/ORM/EntityRelationship/StandardEntityFactoryRelationshipTest.php @@ -2,6 +2,15 @@ declare(strict_types=1); +/* + * This file is part of the zenstruck/foundry package. + * + * (c) Kevin Bond + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Zenstruck\Foundry\Tests\Integration\ORM\EntityRelationship; use PHPUnit\Framework\Attributes\RequiresPhpunit; diff --git a/tests/Unit/FactoryTest.php b/tests/Unit/FactoryTest.php index 842a4c288..153c8054e 100644 --- a/tests/Unit/FactoryTest.php +++ b/tests/Unit/FactoryTest.php @@ -24,8 +24,8 @@ use Zenstruck\Foundry\Tests\Fixture\Entity\GenericEntity; use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\Address\ProxyAddressFactory; use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\Category\CategoryFactory; -use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\Contact\ProxyContactFactory; 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\GenericProxyEntityFactory; use Zenstruck\Foundry\Tests\Fixture\Object1; From fd2e38c547300c2fe2b2a4facb2297c0201756da Mon Sep 17 00:00:00 2001 From: Nicolas PHILIPPE Date: Sat, 14 Dec 2024 23:08:52 +0100 Subject: [PATCH 049/102] chore: upgrade to phpstan 2 (#748) --- bin/tools/phpstan/composer.json | 12 +- bin/tools/phpstan/composer.lock | 124 +++++++++--------- phpstan.neon | 52 +++++++- src/Configuration.php | 2 + src/LazyValue.php | 8 +- src/Maker/Factory/FactoryClassMap.php | 6 +- src/Maker/Factory/FactoryGenerator.php | 5 +- src/Maker/Factory/MakeFactoryData.php | 5 +- src/Maker/Factory/MakeFactoryPHPDocMethod.php | 7 +- src/Object/Instantiator.php | 6 +- src/Persistence/IsProxy.php | 2 +- src/Persistence/PersistenceManager.php | 9 +- src/Persistence/ProxyRepositoryDecorator.php | 13 +- src/Persistence/RepositoryDecorator.php | 12 +- src/Story.php | 2 +- src/Test/Factories.php | 16 +-- src/Test/ResetDatabase.php | 4 +- src/functions.php | 4 + stubs/phpstan/ObjectFactory.php | 19 +-- stubs/phpstan/PersistentObjectFactory.php | 37 +++--- .../phpstan/PersistentProxyObjectFactory.php | 37 +++--- stubs/phpstan/functions.php | 2 +- stubs/psalm/PersistentObjectFactory.php | 8 +- stubs/psalm/PersistentProxyObjectFactory.php | 8 +- ...hangesEntityRelationshipCascadePersist.php | 2 +- .../Fixture/Document/DocumentWithReadonly.php | 6 +- .../InverseSide.php | 10 +- .../ManyToOneToSelfReferencing/OwningSide.php | 2 +- .../SelfReferencingInverseSide.php | 18 ++- .../OwningSideEntity.php | 2 + .../OwningSide.php | 1 + tests/Fixture/Factories/ArrayFactory.php | 2 +- .../Entity/Address/AddressFactory.php | 2 +- .../Entity/Address/ProxyAddressFactory.php | 2 +- .../Entity/Category/CategoryFactory.php | 2 +- .../Entity/Category/ProxyCategoryFactory.php | 2 +- .../Entity/Contact/ContactFactory.php | 2 +- .../Entity/Contact/ProxyContactFactory.php | 2 +- .../InversedSideEntityFactory.php | 2 +- .../OwningSideEntityFactory.php | 2 +- .../Factories/Entity/Tag/ProxyTagFactory.php | 2 +- .../Factories/Entity/Tag/TagFactory.php | 2 +- .../Fixture/Factories/GenericModelFactory.php | 2 +- .../Factories/GenericProxyModelFactory.php | 2 +- tests/Fixture/Factories/Object1Factory.php | 2 +- tests/Fixture/Factories/Object2Factory.php | 2 +- .../WithHooksInInitializeFactory.php | 2 +- ...ate_factory_for_entity_with_repository.php | 2 +- ...ysis_annotations_with_data_set_phpstan.php | 2 +- ...alysis_annotations_with_data_set_psalm.php | 2 +- ...oviderWithProxyFactoryInKernelTestCase.php | 1 - .../ProxyEntityFactoryRelationshipTest.php | 2 +- tests/Unit/FactoryTest.php | 2 +- 53 files changed, 280 insertions(+), 204 deletions(-) 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 948dea282..d1e48b6e7 100644 --- a/bin/tools/phpstan/composer.lock +++ b/bin/tools/phpstan/composer.lock @@ -4,31 +4,31 @@ "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" }, @@ -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.12", + "version": "2.0.3", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "b5ae1b88f471d3fd4ba1aa0046234b5ca3776dd0" + "reference": "46b4d3529b12178112d9008337beda0cc2a1a6b4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/b5ae1b88f471d3fd4ba1aa0046234b5ca3776dd0", - "reference": "b5ae1b88f471d3fd4ba1aa0046234b5ca3776dd0", + "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-11-28T22:13:23+00:00" + "time": "2024-11-28T22:19:37+00:00" }, { "name": "phpstan/phpstan-doctrine", - "version": "1.5.7", + "version": "2.0.1", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-doctrine.git", - "reference": "231d3f795ed5ef54c98961fd3958868cbe091207" + "reference": "bdb6a835c5aa9725979694ae9b70591e180f4853" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-doctrine/zipball/231d3f795ed5ef54c98961fd3958868cbe091207", - "reference": "231d3f795ed5ef54c98961fd3958868cbe091207", + "url": "https://api.github.com/repos/phpstan/phpstan-doctrine/zipball/bdb6a835c5aa9725979694ae9b70591e180f4853", + "reference": "bdb6a835c5aa9725979694ae9b70591e180f4853", "shasum": "" }, "require": { - "php": "^7.2 || ^8.0", - "phpstan/phpstan": "^1.12.12" + "php": "^7.4 || ^8.0", + "phpstan/phpstan": "^2.0.3" }, "conflict": { "doctrine/collections": "<1.0", @@ -202,20 +207,19 @@ "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", + "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.7" + "source": "https://github.com/phpstan/phpstan-doctrine/tree/2.0.1" }, - "time": "2024-12-02T16:47:26+00:00" + "time": "2024-12-02T16:48:00+00:00" }, { "name": "phpstan/phpstan-phpunit", - "version": "1.4.1", + "version": "2.0.1", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-phpunit.git", - "reference": "11d4235fbc6313ecbf93708606edfd3222e44949" + "reference": "4b6ad7fab8683ff4efd7887ba26ef8ee171c7475" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-phpunit/zipball/11d4235fbc6313ecbf93708606edfd3222e44949", - "reference": "11d4235fbc6313ecbf93708606edfd3222e44949", + "url": "https://api.github.com/repos/phpstan/phpstan-phpunit/zipball/4b6ad7fab8683ff4efd7887ba26ef8ee171c7475", + "reference": "4b6ad7fab8683ff4efd7887ba26ef8ee171c7475", "shasum": "" }, "require": { - "php": "^7.2 || ^8.0", - "phpstan/phpstan": "^1.12" + "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.1" + "source": "https://github.com/phpstan/phpstan-phpunit/tree/2.0.1" }, - "time": "2024-11-12T12:43:59+00:00" + "time": "2024-11-12T12:48:00+00:00" }, { "name": "phpstan/phpstan-symfony", - "version": "1.4.12", + "version": "2.0.0", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-symfony.git", - "reference": "c7b7e7f520893621558bfbfdb2694d4364565c1d" + "reference": "1ef4dce2baabd464c2dd3109d051bad94efa1e79" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-symfony/zipball/c7b7e7f520893621558bfbfdb2694d4364565c1d", - "reference": "c7b7e7f520893621558bfbfdb2694d4364565c1d", + "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,9 +367,9 @@ "description": "Symfony Framework extensions and rules for PHPStan", "support": { "issues": "https://github.com/phpstan/phpstan-symfony/issues", - "source": "https://github.com/phpstan/phpstan-symfony/tree/1.4.12" + "source": "https://github.com/phpstan/phpstan-symfony/tree/2.0.0" }, - "time": "2024-11-06T10:13:18+00:00" + "time": "2024-11-06T10:13:40+00:00" } ], "packages-dev": [], 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/src/Configuration.php b/src/Configuration.php index 551d9feff..1dae202ab 100644 --- a/src/Configuration.php +++ b/src/Configuration.php @@ -93,11 +93,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/LazyValue.php b/src/LazyValue.php index b9219714e..f3f5b6e86 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,11 +54,17 @@ 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); 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..533629dee 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; } 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/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/Persistence/IsProxy.php b/src/Persistence/IsProxy.php index a49967f6b..25aba54f2 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 = []; diff --git a/src/Persistence/PersistenceManager.php b/src/Persistence/PersistenceManager.php index 6ab3643cb..f67df2741 100644 --- a/src/Persistence/PersistenceManager.php +++ b/src/Persistence/PersistenceManager.php @@ -168,7 +168,7 @@ 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(); } @@ -240,7 +240,10 @@ public function inverseRelationshipMetadata(string $parent, string $child, strin } /** - * @param class-string $class + * @template T of object + * + * @param class-string $class + * @return ClassMetadata */ public function metadataFor(string $class): ClassMetadata { @@ -248,7 +251,7 @@ public function metadataFor(string $class): ClassMetadata } /** - * @return iterable + * @return iterable> */ public function allMetadata(): iterable { 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..51a4bc346 100644 --- a/src/Persistence/RepositoryDecorator.php +++ b/src/Persistence/RepositoryDecorator.php @@ -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/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 c7d1fd026..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,9 +44,9 @@ 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; @@ -51,10 +54,10 @@ protected function defaults(): array|callable 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()); -assertType("array", UserObjectFactory::new()->many(2)->all()); +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 66af814c8..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,13 +46,13 @@ 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; @@ -57,10 +60,10 @@ protected function defaults(): array|callable 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()); -assertType("array", UserFactory::new()->many(2)->all()); +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(); @@ -73,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 b8044aac3..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,13 +47,13 @@ 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; @@ -58,10 +61,10 @@ protected function defaults(): array|callable 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("array", UserProxyFactory::new()->many(2)->create()); -assertType("array", UserProxyFactory::new()->range(1, 2)->create()); -assertType("array", UserProxyFactory::new()->sequence([])->create()); -assertType("array", UserProxyFactory::new()->many(2)->all()); +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(); @@ -74,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/PersistentObjectFactory.php b/stubs/psalm/PersistentObjectFactory.php index 6a6b471fb..0cf71db9c 100644 --- a/stubs/psalm/PersistentObjectFactory.php +++ b/stubs/psalm/PersistentObjectFactory.php @@ -104,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 51b968e6e..9525a31de 100644 --- a/stubs/psalm/PersistentProxyObjectFactory.php +++ b/stubs/psalm/PersistentProxyObjectFactory.php @@ -105,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/ChangesEntityRelationshipCascadePersist.php b/tests/Fixture/DoctrineCascadeRelationship/ChangesEntityRelationshipCascadePersist.php index 1577ce343..9fcb82458 100644 --- a/tests/Fixture/DoctrineCascadeRelationship/ChangesEntityRelationshipCascadePersist.php +++ b/tests/Fixture/DoctrineCascadeRelationship/ChangesEntityRelationshipCascadePersist.php @@ -60,7 +60,7 @@ public function setUpCascadePersistMetadata(): void /** @var ChangeCascadePersistOnLoadClassMetadataListener $changeCascadePersistListener */ $changeCascadePersistListener = self::getContainer()->get(ChangeCascadePersistOnLoadClassMetadataListener::class); - $changeCascadePersistListener->withMetadata($this->providedData()); + $changeCascadePersistListener->withMetadata(array_values($this->providedData())); /** @var CacheItemPoolInterface $doctrineMetadataCache */ $doctrineMetadataCache = self::getContainer()->get('doctrine.orm.default_metadata_cache'); diff --git a/tests/Fixture/Document/DocumentWithReadonly.php b/tests/Fixture/Document/DocumentWithReadonly.php index 6393bdee4..6fdda7b03 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, diff --git a/tests/Fixture/Entity/EdgeCases/InversedOneToOneWithNonNullableOwning/InverseSide.php b/tests/Fixture/Entity/EdgeCases/InversedOneToOneWithNonNullableOwning/InverseSide.php index 71537fa6c..450b0899d 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 +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/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/RichDomainMandatoryRelationship/OwningSide.php b/tests/Fixture/Entity/EdgeCases/RichDomainMandatoryRelationship/OwningSide.php index 6001e617c..a73fbbd39 100644 --- a/tests/Fixture/Entity/EdgeCases/RichDomainMandatoryRelationship/OwningSide.php +++ b/tests/Fixture/Entity/EdgeCases/RichDomainMandatoryRelationship/OwningSide.php @@ -22,6 +22,7 @@ class OwningSide extends Base { public function __construct( #[ORM\ManyToOne(targetEntity: InversedSide::class, inversedBy: 'relations')] + #[ORM\JoinColumn(nullable: false)] private InversedSide $main, ) { $main->addRelation($this); 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/AddressFactory.php b/tests/Fixture/Factories/Entity/Address/AddressFactory.php index b7fa6013d..31c308f9a 100644 --- a/tests/Fixture/Factories/Entity/Address/AddressFactory.php +++ b/tests/Fixture/Factories/Entity/Address/AddressFactory.php @@ -26,7 +26,7 @@ public static function class(): string 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 f61838bf0..094f138ff 100644 --- a/tests/Fixture/Factories/Entity/Address/ProxyAddressFactory.php +++ b/tests/Fixture/Factories/Entity/Address/ProxyAddressFactory.php @@ -26,7 +26,7 @@ public static function class(): string return Address::class; } - protected function defaults(): array|callable + protected function defaults(): array { return [ 'city' => self::faker()->city(), diff --git a/tests/Fixture/Factories/Entity/Category/CategoryFactory.php b/tests/Fixture/Factories/Entity/Category/CategoryFactory.php index e589575dc..d0846aa11 100644 --- a/tests/Fixture/Factories/Entity/Category/CategoryFactory.php +++ b/tests/Fixture/Factories/Entity/Category/CategoryFactory.php @@ -26,7 +26,7 @@ public static function class(): string 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/ProxyCategoryFactory.php b/tests/Fixture/Factories/Entity/Category/ProxyCategoryFactory.php index 4a7793cb7..22016a573 100644 --- a/tests/Fixture/Factories/Entity/Category/ProxyCategoryFactory.php +++ b/tests/Fixture/Factories/Entity/Category/ProxyCategoryFactory.php @@ -26,7 +26,7 @@ public static function class(): string return Category::class; } - protected function defaults(): array|callable + protected function defaults(): array { return [ 'name' => self::faker()->word(), diff --git a/tests/Fixture/Factories/Entity/Contact/ContactFactory.php b/tests/Fixture/Factories/Entity/Contact/ContactFactory.php index 3e06da2ad..4058ffd23 100644 --- a/tests/Fixture/Factories/Entity/Contact/ContactFactory.php +++ b/tests/Fixture/Factories/Entity/Contact/ContactFactory.php @@ -28,7 +28,7 @@ public static function class(): string 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/ProxyContactFactory.php b/tests/Fixture/Factories/Entity/Contact/ProxyContactFactory.php index af45e5d44..1871b8ccb 100644 --- a/tests/Fixture/Factories/Entity/Contact/ProxyContactFactory.php +++ b/tests/Fixture/Factories/Entity/Contact/ProxyContactFactory.php @@ -28,7 +28,7 @@ public static function class(): string return Contact::class; } - protected function defaults(): array|callable + protected function defaults(): array { return [ 'name' => self::faker()->word(), 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/ProxyTagFactory.php b/tests/Fixture/Factories/Entity/Tag/ProxyTagFactory.php index 5e94d3105..ca73bd963 100644 --- a/tests/Fixture/Factories/Entity/Tag/ProxyTagFactory.php +++ b/tests/Fixture/Factories/Entity/Tag/ProxyTagFactory.php @@ -26,7 +26,7 @@ public static function class(): string 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/TagFactory.php b/tests/Fixture/Factories/Entity/Tag/TagFactory.php index 9a088a00f..8f4854754 100644 --- a/tests/Fixture/Factories/Entity/Tag/TagFactory.php +++ b/tests/Fixture/Factories/Entity/Tag/TagFactory.php @@ -26,7 +26,7 @@ public static function class(): string 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 index a71744e47..f52510a97 100644 --- a/tests/Fixture/Factories/WithHooksInInitializeFactory.php +++ b/tests/Fixture/Factories/WithHooksInInitializeFactory.php @@ -27,7 +27,7 @@ public static function class(): string return Address::class; } - protected function defaults(): array|callable + protected function defaults(): array { return [ 'city' => self::faker()->city(), 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_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 eb57e2f05..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 @@ -44,7 +44,7 @@ * @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 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_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 6c1020360..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 @@ -44,7 +44,7 @@ * @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 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) diff --git a/tests/Integration/DataProvider/DataProviderWithProxyFactoryInKernelTestCase.php b/tests/Integration/DataProvider/DataProviderWithProxyFactoryInKernelTestCase.php index 79da54529..7ba7918a7 100644 --- a/tests/Integration/DataProvider/DataProviderWithProxyFactoryInKernelTestCase.php +++ b/tests/Integration/DataProvider/DataProviderWithProxyFactoryInKernelTestCase.php @@ -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/ORM/EntityRelationship/ProxyEntityFactoryRelationshipTest.php b/tests/Integration/ORM/EntityRelationship/ProxyEntityFactoryRelationshipTest.php index ccd2a631c..12db0daaa 100644 --- a/tests/Integration/ORM/EntityRelationship/ProxyEntityFactoryRelationshipTest.php +++ b/tests/Integration/ORM/EntityRelationship/ProxyEntityFactoryRelationshipTest.php @@ -46,7 +46,7 @@ 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(); diff --git a/tests/Unit/FactoryTest.php b/tests/Unit/FactoryTest.php index 153c8054e..925cde61a 100644 --- a/tests/Unit/FactoryTest.php +++ b/tests/Unit/FactoryTest.php @@ -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']]); From 09d548eb7c09a6ab7b4031b9c6fc1bfd807bf571 Mon Sep 17 00:00:00 2001 From: nikophil Date: Sat, 14 Dec 2024 22:12:39 +0000 Subject: [PATCH 050/102] bot: fix cs [skip ci] --- src/Persistence/PersistenceManager.php | 2 +- src/Persistence/RepositoryDecorator.php | 4 ++-- .../ChangesEntityRelationshipCascadePersist.php | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Persistence/PersistenceManager.php b/src/Persistence/PersistenceManager.php index f67df2741..c58d4b8de 100644 --- a/src/Persistence/PersistenceManager.php +++ b/src/Persistence/PersistenceManager.php @@ -242,7 +242,7 @@ public function inverseRelationshipMetadata(string $parent, string $child, strin /** * @template T of object * - * @param class-string $class + * @param class-string $class * @return ClassMetadata */ public function metadataFor(string $class): ClassMetadata diff --git a/src/Persistence/RepositoryDecorator.php b/src/Persistence/RepositoryDecorator.php index 51a4bc346..8d4076dcd 100644 --- a/src/Persistence/RepositoryDecorator.php +++ b/src/Persistence/RepositoryDecorator.php @@ -117,7 +117,7 @@ public function findOrFail(mixed $id): object */ public function findAll(): array { - return array_values($this->inner()->findAll()); + return \array_values($this->inner()->findAll()); } /** @@ -128,7 +128,7 @@ public function findAll(): array */ public function findBy(array $criteria, ?array $orderBy = null, $limit = null, $offset = null): array { - return array_values($this->inner()->findBy($this->normalize($criteria), $orderBy, $limit, $offset)); + return \array_values($this->inner()->findBy($this->normalize($criteria), $orderBy, $limit, $offset)); } /** diff --git a/tests/Fixture/DoctrineCascadeRelationship/ChangesEntityRelationshipCascadePersist.php b/tests/Fixture/DoctrineCascadeRelationship/ChangesEntityRelationshipCascadePersist.php index 9fcb82458..a244beec4 100644 --- a/tests/Fixture/DoctrineCascadeRelationship/ChangesEntityRelationshipCascadePersist.php +++ b/tests/Fixture/DoctrineCascadeRelationship/ChangesEntityRelationshipCascadePersist.php @@ -60,7 +60,7 @@ public function setUpCascadePersistMetadata(): void /** @var ChangeCascadePersistOnLoadClassMetadataListener $changeCascadePersistListener */ $changeCascadePersistListener = self::getContainer()->get(ChangeCascadePersistOnLoadClassMetadataListener::class); - $changeCascadePersistListener->withMetadata(array_values($this->providedData())); + $changeCascadePersistListener->withMetadata(\array_values($this->providedData())); /** @var CacheItemPoolInterface $doctrineMetadataCache */ $doctrineMetadataCache = self::getContainer()->get('doctrine.orm.default_metadata_cache'); From d1240b1fea4b98e86ca41c5a58602a19058c8aac Mon Sep 17 00:00:00 2001 From: Nicolas PHILIPPE Date: Mon, 16 Dec 2024 08:35:15 +0100 Subject: [PATCH 051/102] fix: RequiresPhpunit should use semver constraint --- tests/Integration/ORM/EdgeCasesRelationshipTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Integration/ORM/EdgeCasesRelationshipTest.php b/tests/Integration/ORM/EdgeCasesRelationshipTest.php index 9a0739fc6..babb0a930 100644 --- a/tests/Integration/ORM/EdgeCasesRelationshipTest.php +++ b/tests/Integration/ORM/EdgeCasesRelationshipTest.php @@ -44,7 +44,7 @@ final class EdgeCasesRelationshipTest extends KernelTestCase #[Test] #[DataProvider('provideCascadeRelationshipsCombinations')] #[UsingRelationships(RelationshipWithGlobalEntity\RelationshipWithGlobalEntity::class, ['globalEntity'])] - #[RequiresPhpunit('11.4')] + #[RequiresPhpunit('^11.4')] public function it_can_use_flush_after_and_entity_from_global_state(): void { $relationshipWithGlobalEntityFactory = persistent_factory(RelationshipWithGlobalEntity\RelationshipWithGlobalEntity::class); @@ -71,7 +71,7 @@ public function it_can_use_flush_after_and_entity_from_global_state(): void #[Test] #[DataProvider('provideCascadeRelationshipsCombinations')] #[UsingRelationships(RichDomainMandatoryRelationship\OwningSide::class, ['main'])] - #[RequiresPhpunit('11.4')] + #[RequiresPhpunit('^11.4')] public function inversed_relationship_mandatory(): void { $owningSideEntityFactory = persistent_factory(RichDomainMandatoryRelationship\OwningSide::class); From ff7210a70ec418858bd8f994834d1f1fc9d6b5bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=93=D1=80=D0=B8=D0=B3=D0=BE=D1=80=D0=B8=D0=B9?= Date: Tue, 17 Dec 2024 20:11:23 +0300 Subject: [PATCH 052/102] [Docs fix] Factory::_real() instead Factory::object() (#759) Relying on docs I was trying to call such a method as "Factory::object()" but there was no this method. Maybe the "Factory::_real()" was ment --- docs/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.rst b/docs/index.rst index b341eb915..e4fd55b43 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -720,7 +720,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 From d192c4a8955a91617492a0374dd7848023bdf6ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=93=D1=80=D0=B8=D0=B3=D0=BE=D1=80=D0=B8=D0=B9?= Date: Tue, 17 Dec 2024 20:13:45 +0300 Subject: [PATCH 053/102] [Docs fix] Proxy::_save() instead of Proxy::save() (#760) Changed an incorrect function name in the docs $post->save(); // persist the Post to $post->_save(); // persist the Post --- docs/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.rst b/docs/index.rst index e4fd55b43..8d423aec1 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1051,7 +1051,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 From cafc6932ae87c300d78ace93bb0fc462a0226d38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=93=D1=80=D0=B8=D0=B3=D0=BE=D1=80=D0=B8=D0=B9?= Date: Tue, 17 Dec 2024 20:13:54 +0300 Subject: [PATCH 054/102] [Docs fix] Just spelling in docs (#761) "Then the command" instead of "Then then command" --- docs/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.rst b/docs/index.rst index 8d423aec1..af794d247 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1305,7 +1305,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:: From 1db5cedebd5d048ac04642093861604f4c0cdb23 Mon Sep 17 00:00:00 2001 From: Nicolas PHILIPPE Date: Tue, 17 Dec 2024 18:57:11 +0100 Subject: [PATCH 055/102] tests: validate PSR-4 in CI (#762) --- .github/workflows/ci.yml | 3 +++ composer.json | 3 ++- tests/Unit/Persistence/ProxyGeneratorTest.php | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9c232ee52..ab70a16df 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -274,6 +274,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 diff --git a/composer.json b/composer.json index 490e79bb4..73cf9ec6f 100644 --- a/composer.json +++ b/composer.json @@ -61,7 +61,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", 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; From e8f9a9266a73f9bc27b2018529eeb34bce20cc10 Mon Sep 17 00:00:00 2001 From: Nicolas PHILIPPE Date: Fri, 20 Dec 2024 13:59:25 +0100 Subject: [PATCH 056/102] docs: clarify default attributes and fixed some syntax issues (#765) Co-authored-by: Javier Eguiluz --- docs/index.rst | 61 +++++++++++++++++++++++++++++++++++++------------- 1 file changed, 45 insertions(+), 16 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index af794d247..a1a82ca96 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 `https://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 @@ -616,15 +630,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: :: @@ -655,16 +670,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): @@ -843,8 +871,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. @@ -2216,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 From cf3cc8ba06131e6800ca4cca5e385d8746d27e54 Mon Sep 17 00:00:00 2001 From: Javier Eguiluz Date: Sat, 21 Dec 2024 00:21:21 +0100 Subject: [PATCH 057/102] docs: Minor syntax fix (#767) --- docs/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.rst b/docs/index.rst index a1a82ca96..03c13c141 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1770,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: :: From 9948d6a181a1a2483ae2cbe0b9a74d5861c7ba05 Mon Sep 17 00:00:00 2001 From: Nicolas PHILIPPE Date: Sat, 21 Dec 2024 17:30:37 +0100 Subject: [PATCH 058/102] fix(ci): change PHP version used by PHP CS-Fixer (#768) --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ab70a16df..954bf3061 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -297,7 +297,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 }} From 9810cbbaface4835969a685f661e89029afbae12 Mon Sep 17 00:00:00 2001 From: nikophil Date: Sat, 21 Dec 2024 16:33:44 +0000 Subject: [PATCH 059/102] bot: fix cs [skip ci] --- src/Attribute/WithStory.php | 14 +++++++-- src/Factory.php | 24 ++++++-------- src/FactoryCollection.php | 6 ++-- src/LazyValue.php | 2 +- src/Maker/Factory/MakeFactoryData.php | 2 +- .../ObjectDefaultPropertiesGuesser.php | 2 +- src/ORM/ResetDatabase/BaseOrmResetter.php | 12 +++++-- .../ResetDatabase/MigrateDatabaseResetter.php | 15 ++++++--- src/ORM/ResetDatabase/ResetDatabaseMode.php | 9 ++++++ src/Persistence/PersistenceStrategy.php | 2 +- src/Persistence/RepositoryDecorator.php | 2 +- .../DoctrineCascadeRelationshipMetadata.php | 31 ++++++++++++------- .../UsingRelationships.php | 14 +++++++-- .../Fixture/Document/DocumentWithReadonly.php | 3 +- .../EntityWithReadonly/EntityWithReadonly.php | 3 +- tests/Fixture/ObjectWithEnum.php | 14 +++++++-- tests/Fixture/Stories/ServiceStory.php | 5 ++- .../GenericProxyFactoryTestCase.php | 4 +-- tests/Unit/ObjectFactoryTest.php | 12 +++---- 19 files changed, 113 insertions(+), 63 deletions(-) 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/Factory.php b/src/Factory.php index 8b4f18a1f..c4489582a 100644 --- a/src/Factory.php +++ b/src/Factory.php @@ -25,16 +25,15 @@ */ abstract class Factory { - /** @phpstan-var Attributes[] */ - private array $attributes; - /** - * Memoization of normalized parameters + * Memoization of normalized parameters. * * @internal * @var Parameters|null */ - protected array|null $normalizedParameters = null; + protected ?array $normalizedParameters = null; + /** @phpstan-var Attributes[] */ + private array $attributes; // keep an empty constructor for BC public function __construct() @@ -42,7 +41,6 @@ public function __construct() } /** - * @return static * @phpstan-return static * @phpstan-param Attributes $attributes */ @@ -161,8 +159,6 @@ final protected static function faker(): Faker\Generator /** * Override to adjust default attributes & config. - * - * @return static */ protected function initialize(): static { @@ -212,9 +208,9 @@ protected function initializeInternal(): static */ protected function normalizeParameters(array $parameters): array { - return $this->normalizedParameters = 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) ); } @@ -240,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) ); } diff --git a/src/FactoryCollection.php b/src/FactoryCollection.php index d2f70c763..6d8f20592 100644 --- a/src/FactoryCollection.php +++ b/src/FactoryCollection.php @@ -37,7 +37,7 @@ private function __construct(public readonly Factory $factory, private \Closure */ 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; } @@ -47,7 +47,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; } } @@ -96,7 +96,7 @@ public static function range(Factory $factory, int $min, int $max): self } /** - * @param TFactory $factory + * @param TFactory $factory * @phpstan-param iterable $items * @return self */ diff --git a/src/LazyValue.php b/src/LazyValue.php index f3f5b6e86..53312272e 100644 --- a/src/LazyValue.php +++ b/src/LazyValue.php @@ -71,7 +71,7 @@ public static function memoize(callable $factory): self } /** - * @param array $value + * @param array $value * @return array */ private static function normalizeArray(array $value): array diff --git a/src/Maker/Factory/MakeFactoryData.php b/src/Maker/Factory/MakeFactoryData.php index 533629dee..fc9311349 100644 --- a/src/Maker/Factory/MakeFactoryData.php +++ b/src/Maker/Factory/MakeFactoryData.php @@ -177,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/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/ORM/ResetDatabase/BaseOrmResetter.php b/src/ORM/ResetDatabase/BaseOrmResetter.php index c1a2a3d4a..43e182d38 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; @@ -11,7 +20,6 @@ use Symfony\Bundle\FrameworkBundle\Console\Application; use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\HttpKernel\KernelInterface; - use Zenstruck\Foundry\ORM\DoctrineOrmVersionGuesser; use function Zenstruck\Foundry\runCommand; @@ -61,7 +69,7 @@ final protected function dropAndResetDatabase(Application $application): void $dbPath = $connection->getParams()['path'] ?? null; if (DoctrineOrmVersionGuesser::isOrmV3() && $dbPath && (new Filesystem())->exists($dbPath)) { - file_put_contents($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/Persistence/PersistenceStrategy.php b/src/Persistence/PersistenceStrategy.php index 3e911f491..13faaa7a0 100644 --- a/src/Persistence/PersistenceStrategy.php +++ b/src/Persistence/PersistenceStrategy.php @@ -70,7 +70,7 @@ public function inversedRelationshipMetadata(string $parent, string $child, stri /** * @template T of object - * @param class-string $class + * @param class-string $class * @return ClassMetadata * * @throws MappingException If $class is not managed by Doctrine diff --git a/src/Persistence/RepositoryDecorator.php b/src/Persistence/RepositoryDecorator.php index 8d4076dcd..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); diff --git a/tests/Fixture/DoctrineCascadeRelationship/DoctrineCascadeRelationshipMetadata.php b/tests/Fixture/DoctrineCascadeRelationship/DoctrineCascadeRelationshipMetadata.php index e860a3f14..ffc2be412 100644 --- a/tests/Fixture/DoctrineCascadeRelationship/DoctrineCascadeRelationshipMetadata.php +++ b/tests/Fixture/DoctrineCascadeRelationship/DoctrineCascadeRelationshipMetadata.php @@ -2,6 +2,15 @@ declare(strict_types=1); +/* + * This file is part of the zenstruck/foundry package. + * + * (c) Kevin Bond + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Zenstruck\Foundry\Tests\Fixture\DoctrineCascadeRelationship; /** @@ -16,6 +25,11 @@ private function __construct( ) { } + public function __toString(): string + { + return \sprintf('%s::$%s - %s', $this->class, $this->field, $this->cascade ? 'cascade' : 'no cascade'); + } + /** * @param array{class: class-string, field: string} $source */ @@ -24,34 +38,29 @@ public static function fromArray(array $source, bool $cascade = false): self return new self(class: $source['class'], field: $source['field'], cascade: $cascade); } - public function __toString(): string - { - return \sprintf('%s::$%s - %s', $this->class, $this->field, $this->cascade ? 'cascade' : 'no cascade'); - } - /** - * @param list $relationshipFields + * @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 = DoctrineCascadeRelationshipMetadata::fromArray($relationshipFields[0]); + $metadata = self::fromArray($relationshipFields[0]); yield "{$metadata}\n" => [$metadata]; return; } - $total = pow(2, count($relationshipFields)); + $total = 2 ** \count($relationshipFields); - for ($i = 0; $i < $total; $i++) { + for ($i = 0; $i < $total; ++$i) { $temp = []; $permutationName = "\n"; - for ($j = 0; $j < count($relationshipFields); $j++) { - $metadata = DoctrineCascadeRelationshipMetadata::fromArray($relationshipFields[$j], cascade: (bool)(($i >> $j) & 1)); + for ($j = 0; $j < \count($relationshipFields); ++$j) { + $metadata = self::fromArray($relationshipFields[$j], cascade: (bool) (($i >> $j) & 1)); $temp[] = $metadata; $permutationName = "{$permutationName}$metadata\n"; diff --git a/tests/Fixture/DoctrineCascadeRelationship/UsingRelationships.php b/tests/Fixture/DoctrineCascadeRelationship/UsingRelationships.php index 71306a616..7041d271c 100644 --- a/tests/Fixture/DoctrineCascadeRelationship/UsingRelationships.php +++ b/tests/Fixture/DoctrineCascadeRelationship/UsingRelationships.php @@ -2,6 +2,15 @@ declare(strict_types=1); +/* + * This file is part of the zenstruck/foundry package. + * + * (c) Kevin Bond + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Zenstruck\Foundry\Tests\Fixture\DoctrineCascadeRelationship; /** @@ -13,8 +22,7 @@ final class UsingRelationships public function __construct( /** @var class-string */ public readonly string $class, - public readonly array $relationShips - ) - { + public readonly array $relationShips, + ) { } } diff --git a/tests/Fixture/Document/DocumentWithReadonly.php b/tests/Fixture/Document/DocumentWithReadonly.php index 6fdda7b03..8a775edf0 100644 --- a/tests/Fixture/Document/DocumentWithReadonly.php +++ b/tests/Fixture/Document/DocumentWithReadonly.php @@ -27,7 +27,6 @@ public function __construct( #[MongoDB\Field()] public readonly \DateTimeImmutable $date, - ) - { + ) { } } 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/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/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/Integration/Persistence/GenericProxyFactoryTestCase.php b/tests/Integration/Persistence/GenericProxyFactoryTestCase.php index b8cdd16c8..014ed2a0f 100644 --- a/tests/Integration/Persistence/GenericProxyFactoryTestCase.php +++ b/tests/Integration/Persistence/GenericProxyFactoryTestCase.php @@ -290,12 +290,12 @@ public function can_use_after_persist_with_attributes_added_in_before_instantiat $value = 'value set with before instantiate'; $object = $this->factory() ->instantiateWith(Instantiator::withConstructor()->allowExtra('extra')) - ->beforeInstantiate(function (array $attributes) use ($value) { + ->beforeInstantiate(function(array $attributes) use ($value) { $attributes['extra'] = $value; return $attributes; }) - ->afterPersist(function (GenericModel $object, array $attributes) { + ->afterPersist(function(GenericModel $object, array $attributes) { $object->setProp1($attributes['extra']); }) ->create(); 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}", ]; } - } + }, ]; } From 5f9950650ee027bee169e58278213dec455a0b1f Mon Sep 17 00:00:00 2001 From: Nicolas PHILIPPE Date: Tue, 24 Dec 2024 15:43:17 +0100 Subject: [PATCH 060/102] minor: introduce PerssitenceManager::isPersisted() (#754) --- src/Configuration.php | 9 +++++++-- src/Persistence/IsProxy.php | 13 +++---------- src/Persistence/PersistenceManager.php | 12 ++++++++++++ src/Persistence/PersistentObjectFactory.php | 5 +++-- src/Persistence/PersistentProxyObjectFactory.php | 2 +- .../EntityFactoryRelationshipTestCase.php | 2 +- .../ProxyEntityFactoryRelationshipTest.php | 3 +-- 7 files changed, 28 insertions(+), 18 deletions(-) diff --git a/src/Configuration.php b/src/Configuration.php index 1dae202ab..048604b4c 100644 --- a/src/Configuration.php +++ b/src/Configuration.php @@ -67,9 +67,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.'); } } diff --git a/src/Persistence/IsProxy.php b/src/Persistence/IsProxy.php index 25aba54f2..0c140b2b4 100644 --- a/src/Persistence/IsProxy.php +++ b/src/Persistence/IsProxy.php @@ -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/PersistenceManager.php b/src/Persistence/PersistenceManager.php index c58d4b8de..4eb6433b9 100644 --- a/src/Persistence/PersistenceManager.php +++ b/src/Persistence/PersistenceManager.php @@ -175,6 +175,18 @@ public function refresh(object &$object, bool $force = false): object return $object; } + public function isPersisted(object $object): bool + { + if ($object instanceof Proxy) { + $object = unproxy($object); + } + + $om = $this->strategyFor($object::class)->objectManagerFor($object::class); + $id = $om->getClassMetadata($object::class)->getIdentifierValues($object); + + return $id && $om->find($object::class, $id) !== null; + } + /** * @template T of object * diff --git a/src/Persistence/PersistentObjectFactory.php b/src/Persistence/PersistentObjectFactory.php index 234bd0449..77460b846 100644 --- a/src/Persistence/PersistentObjectFactory.php +++ b/src/Persistence/PersistentObjectFactory.php @@ -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 } @@ -279,11 +279,12 @@ protected function normalizeParameter(string $field, mixed $value): mixed // 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 + // but a cascade:["remove"] would remove too many things $value->create([$inverseField => $object]); $pm->refresh($object); $oldObj = get($inversedObject, $inverseField); 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/tests/Integration/ORM/EntityRelationship/EntityFactoryRelationshipTestCase.php b/tests/Integration/ORM/EntityRelationship/EntityFactoryRelationshipTestCase.php index 4ee765d23..4d004dba6 100644 --- a/tests/Integration/ORM/EntityRelationship/EntityFactoryRelationshipTestCase.php +++ b/tests/Integration/ORM/EntityRelationship/EntityFactoryRelationshipTestCase.php @@ -167,7 +167,7 @@ public function one_to_one_owning(): void #[Test] #[DataProvider('provideCascadeRelationshipsCombinations')] #[UsingRelationships(Address::class, ['contact'])] - #[UsingRelationships(Contact::class, ['address'])] + #[UsingRelationships(Contact::class, ['address', 'category'])] public function inversed_one_to_one(): void { $address = static::addressFactory()->create(['contact' => static::contactFactory()]); diff --git a/tests/Integration/ORM/EntityRelationship/ProxyEntityFactoryRelationshipTest.php b/tests/Integration/ORM/EntityRelationship/ProxyEntityFactoryRelationshipTest.php index 12db0daaa..696bbfa1c 100644 --- a/tests/Integration/ORM/EntityRelationship/ProxyEntityFactoryRelationshipTest.php +++ b/tests/Integration/ORM/EntityRelationship/ProxyEntityFactoryRelationshipTest.php @@ -127,12 +127,11 @@ public function can_remove_and_assert_not_persisted(): void /** @test */ #[Test] - public function cannot_use_assert_persisted_when_entity_has_changes(): void + public function can_use_assert_persisted_when_entity_has_changes(): void { $contact = static::contactFactory()->create(); $contact->setName('foo'); - $this->expectException(RefreshObjectFailed::class); $contact->_assertPersisted(); } From f2955ed5d2664106f6179a629850650a01bf33ba Mon Sep 17 00:00:00 2001 From: nikophil Date: Tue, 24 Dec 2024 14:46:23 +0000 Subject: [PATCH 061/102] bot: fix cs [skip ci] --- src/Persistence/PersistenceManager.php | 2 +- .../EntityRelationship/ProxyEntityFactoryRelationshipTest.php | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Persistence/PersistenceManager.php b/src/Persistence/PersistenceManager.php index 4eb6433b9..94d14babd 100644 --- a/src/Persistence/PersistenceManager.php +++ b/src/Persistence/PersistenceManager.php @@ -184,7 +184,7 @@ public function isPersisted(object $object): bool $om = $this->strategyFor($object::class)->objectManagerFor($object::class); $id = $om->getClassMetadata($object::class)->getIdentifierValues($object); - return $id && $om->find($object::class, $id) !== null; + return $id && null !== $om->find($object::class, $id); } /** diff --git a/tests/Integration/ORM/EntityRelationship/ProxyEntityFactoryRelationshipTest.php b/tests/Integration/ORM/EntityRelationship/ProxyEntityFactoryRelationshipTest.php index 696bbfa1c..98e4faa15 100644 --- a/tests/Integration/ORM/EntityRelationship/ProxyEntityFactoryRelationshipTest.php +++ b/tests/Integration/ORM/EntityRelationship/ProxyEntityFactoryRelationshipTest.php @@ -20,7 +20,6 @@ use PHPUnit\Framework\Attributes\RequiresPhpunit; use PHPUnit\Framework\Attributes\Test; use Zenstruck\Assert; -use Zenstruck\Foundry\Persistence\Exception\RefreshObjectFailed; use Zenstruck\Foundry\Persistence\Proxy; use Zenstruck\Foundry\Tests\Fixture\DoctrineCascadeRelationship\UsingRelationships; use Zenstruck\Foundry\Tests\Fixture\Entity\Contact; From 65cedbfb310fc4c33250ec07707f5c6d5091f0a5 Mon Sep 17 00:00:00 2001 From: Nicolas PHILIPPE Date: Tue, 24 Dec 2024 15:53:32 +0100 Subject: [PATCH 062/102] fix: use a "placeholder" for inversed one-to-one (#755) --- src/Persistence/PersistMode.php | 21 ++++++ src/Persistence/PersistenceManager.php | 11 +++ src/Persistence/PersistentObjectFactory.php | 71 ++++++++++++------- .../OwningSide.php | 8 +-- .../InverseSide.php | 39 ++++++++++ .../InversedOneToOneWithSetter/OwningSide.php | 28 ++++++++ .../ORM/EdgeCasesRelationshipTest.php | 27 ++++++- 7 files changed, 171 insertions(+), 34 deletions(-) create mode 100644 src/Persistence/PersistMode.php create mode 100644 tests/Fixture/Entity/EdgeCases/InversedOneToOneWithSetter/InverseSide.php create mode 100644 tests/Fixture/Entity/EdgeCases/InversedOneToOneWithSetter/OwningSide.php diff --git a/src/Persistence/PersistMode.php b/src/Persistence/PersistMode.php new file mode 100644 index 000000000..7be47b636 --- /dev/null +++ b/src/Persistence/PersistMode.php @@ -0,0 +1,21 @@ + + */ +enum PersistMode +{ + case PERSIST; + case WITHOUT_PERSISTING; + case NO_PERSIST_BUT_SCHEDULE_FOR_INSERT; + + public function isPersisting(): bool + { + return $this === self::PERSIST; + } +} diff --git a/src/Persistence/PersistenceManager.php b/src/Persistence/PersistenceManager.php index 94d14babd..70f6f9850 100644 --- a/src/Persistence/PersistenceManager.php +++ b/src/Persistence/PersistenceManager.php @@ -94,6 +94,17 @@ public function scheduleForInsert(object $object): 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 * diff --git a/src/Persistence/PersistentObjectFactory.php b/src/Persistence/PersistentObjectFactory.php index 77460b846..179c5fc58 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,11 +34,14 @@ */ abstract class PersistentObjectFactory extends ObjectFactory { - private bool $persist; + private PersistMode $persist; /** @phpstan-var list */ private array $afterPersist = []; + /** @var list */ + private array $tempAfterInstantiate = []; + /** @var list */ private array $tempAfterPersist = []; @@ -196,6 +199,12 @@ 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()) { @@ -232,7 +241,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,7 +249,15 @@ final public function andPersist(): static final public function withoutPersisting(): static { $clone = clone $this; - $clone->persist = false; + $clone->persist = PersistMode::WITHOUT_PERSISTING; + + return $clone; + } + + private function withoutPersistingButScheduleForInsert(): static + { + $clone = clone $this; + $clone->persist = PersistMode::NO_PERSIST_BUT_SCHEDULE_FOR_INSERT; return $clone; } @@ -263,9 +280,11 @@ protected function normalizeParameter(string $field, mixed $value): mixed } if ($value instanceof self && isset($this->persist)) { - $value = $this->isPersisting() - ? $value->andPersist() - : $value->withoutPersisting(); + $value = match($this->persist) { + PersistMode::PERSIST => $value->andPersist(), + PersistMode::WITHOUT_PERSISTING => $value->withoutPersisting(), + PersistMode::NO_PERSIST_BUT_SCHEDULE_FOR_INSERT => $value->withoutPersistingButScheduleForInsert(), + }; } if ($value instanceof self) { @@ -277,21 +296,21 @@ protected function normalizeParameter(string $field, mixed $value): mixed if ($inversedRelationshipMetadata && !$inversedRelationshipMetadata->isCollection) { $inverseField = $inversedRelationshipMetadata->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:["remove"] 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 - } + // 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->withoutPersistingButScheduleForInsert() + ->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; @@ -359,11 +378,13 @@ 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()); + $persistMode = $this->persist ?? ($config->persistence()->autoPersist(static::class()) ? PersistMode::PERSIST : PersistMode::WITHOUT_PERSISTING); + + return $persistMode->isPersisting(); } /** @@ -373,7 +394,7 @@ final protected function initializeInternal(): static { return $this->afterInstantiate( static function(object $object, array $parameters, PersistentObjectFactory $factory): void { - if (!$factory->isPersisting()) { + if (!$factory->isPersisting() && (!isset($factory->persist) || $factory->persist !== PersistMode::NO_PERSIST_BUT_SCHEDULE_FOR_INSERT)) { return; } 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/InversedOneToOneWithSetter/InverseSide.php b/tests/Fixture/Entity/EdgeCases/InversedOneToOneWithSetter/InverseSide.php new file mode 100644 index 000000000..1a2ec9d28 --- /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|null $owningSide = null; + + public function getOwningSide(): ?OwningSide + { + return $this->owningSide; + } + + public function setOwningSide(OwningSide $owningSide): void + { + $this->owningSide = $owningSide; + $owningSide->inverseSide = $this; + } +} diff --git a/tests/Fixture/Entity/EdgeCases/InversedOneToOneWithSetter/OwningSide.php b/tests/Fixture/Entity/EdgeCases/InversedOneToOneWithSetter/OwningSide.php new file mode 100644 index 000000000..1c8460f78 --- /dev/null +++ b/tests/Fixture/Entity/EdgeCases/InversedOneToOneWithSetter/OwningSide.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Tests\Fixture\Entity\EdgeCases\InversedOneToOneWithSetter; + +use Doctrine\ORM\Mapping as ORM; +use Zenstruck\Foundry\Tests\Fixture\Model\Base; + +/** + * @author Nicolas PHILIPPE + */ +#[ORM\Entity] +#[ORM\Table('inversed_one_to_one_with_setter_owning_side')] +class OwningSide extends Base +{ + #[ORM\OneToOne(inversedBy: 'owningSide')] + public ?InverseSide $inverseSide = null; +} diff --git a/tests/Integration/ORM/EdgeCasesRelationshipTest.php b/tests/Integration/ORM/EdgeCasesRelationshipTest.php index babb0a930..6d0189df5 100644 --- a/tests/Integration/ORM/EdgeCasesRelationshipTest.php +++ b/tests/Integration/ORM/EdgeCasesRelationshipTest.php @@ -21,6 +21,7 @@ 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\InversedOneToOneWithSetter; use Zenstruck\Foundry\Tests\Fixture\Entity\EdgeCases\InversedOneToOneWithNonNullableOwning; use Zenstruck\Foundry\Tests\Fixture\Entity\EdgeCases\ManyToOneToSelfReferencing; use Zenstruck\Foundry\Tests\Fixture\Entity\EdgeCases\RelationshipWithGlobalEntity; @@ -86,9 +87,11 @@ public function inversed_relationship_mandatory(): void $inversedSideEntityFactory::assert()->count(1); } - /** - * @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); @@ -102,6 +105,24 @@ 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 */ From f3007a1b4c9ba8366fa6664d117ad9e7a4f72a38 Mon Sep 17 00:00:00 2001 From: nikophil Date: Tue, 24 Dec 2024 14:56:36 +0000 Subject: [PATCH 063/102] bot: fix cs [skip ci] --- src/Persistence/PersistMode.php | 11 +++++++++- src/Persistence/PersistentObjectFactory.php | 20 +++++++++---------- .../InverseSide.php | 2 +- .../ORM/EdgeCasesRelationshipTest.php | 2 +- 4 files changed, 22 insertions(+), 13 deletions(-) diff --git a/src/Persistence/PersistMode.php b/src/Persistence/PersistMode.php index 7be47b636..69172cef7 100644 --- a/src/Persistence/PersistMode.php +++ b/src/Persistence/PersistMode.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\Persistence; /** @@ -16,6 +25,6 @@ enum PersistMode public function isPersisting(): bool { - return $this === self::PERSIST; + return self::PERSIST === $this; } } diff --git a/src/Persistence/PersistentObjectFactory.php b/src/Persistence/PersistentObjectFactory.php index 179c5fc58..1167a95ab 100644 --- a/src/Persistence/PersistentObjectFactory.php +++ b/src/Persistence/PersistentObjectFactory.php @@ -254,14 +254,6 @@ final public function withoutPersisting(): static return $clone; } - private function withoutPersistingButScheduleForInsert(): static - { - $clone = clone $this; - $clone->persist = PersistMode::NO_PERSIST_BUT_SCHEDULE_FOR_INSERT; - - return $clone; - } - /** * @phpstan-param callable(T, Parameters, static):void $callback */ @@ -280,7 +272,7 @@ protected function normalizeParameter(string $field, mixed $value): mixed } if ($value instanceof self && isset($this->persist)) { - $value = match($this->persist) { + $value = match ($this->persist) { PersistMode::PERSIST => $value->andPersist(), PersistMode::WITHOUT_PERSISTING => $value->withoutPersisting(), PersistMode::NO_PERSIST_BUT_SCHEDULE_FOR_INSERT => $value->withoutPersistingButScheduleForInsert(), @@ -394,7 +386,7 @@ final protected function initializeInternal(): static { return $this->afterInstantiate( static function(object $object, array $parameters, PersistentObjectFactory $factory): void { - if (!$factory->isPersisting() && (!isset($factory->persist) || $factory->persist !== PersistMode::NO_PERSIST_BUT_SCHEDULE_FOR_INSERT)) { + if (!$factory->isPersisting() && (!isset($factory->persist) || PersistMode::NO_PERSIST_BUT_SCHEDULE_FOR_INSERT !== $factory->persist)) { return; } @@ -403,6 +395,14 @@ static function(object $object, array $parameters, PersistentObjectFactory $fact ); } + private function withoutPersistingButScheduleForInsert(): static + { + $clone = clone $this; + $clone->persist = PersistMode::NO_PERSIST_BUT_SCHEDULE_FOR_INSERT; + + return $clone; + } + private function throwIfCannotCreateObject(): void { $configuration = Configuration::instance(); diff --git a/tests/Fixture/Entity/EdgeCases/InversedOneToOneWithSetter/InverseSide.php b/tests/Fixture/Entity/EdgeCases/InversedOneToOneWithSetter/InverseSide.php index 1a2ec9d28..bc3b91134 100644 --- a/tests/Fixture/Entity/EdgeCases/InversedOneToOneWithSetter/InverseSide.php +++ b/tests/Fixture/Entity/EdgeCases/InversedOneToOneWithSetter/InverseSide.php @@ -24,7 +24,7 @@ class InverseSide extends Base { #[ORM\OneToOne(mappedBy: 'inverseSide')] - private OwningSide|null $owningSide = null; + private ?OwningSide $owningSide = null; public function getOwningSide(): ?OwningSide { diff --git a/tests/Integration/ORM/EdgeCasesRelationshipTest.php b/tests/Integration/ORM/EdgeCasesRelationshipTest.php index 6d0189df5..1bf563f33 100644 --- a/tests/Integration/ORM/EdgeCasesRelationshipTest.php +++ b/tests/Integration/ORM/EdgeCasesRelationshipTest.php @@ -21,8 +21,8 @@ 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\InversedOneToOneWithSetter; use Zenstruck\Foundry\Tests\Fixture\Entity\EdgeCases\InversedOneToOneWithNonNullableOwning; +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; From 5a93ba5dd2b078c2ad870c9ba970796c33d39dc7 Mon Sep 17 00:00:00 2001 From: kbond Date: Wed, 1 Jan 2025 01:25:24 +0000 Subject: [PATCH 064/102] bot: fix cs [skip ci] --- tests/Unit/LazyValueTest.php | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) 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()); } From f303f3f8b6d4d06634eba8f04b0e14624df6904f Mon Sep 17 00:00:00 2001 From: Nicolas PHILIPPE Date: Thu, 2 Jan 2025 18:55:27 +0100 Subject: [PATCH 065/102] fix: remove _refresh call from create object process (#773) --- src/Persistence/PersistenceManager.php | 24 ++++++++++++++++++--- src/Persistence/PersistentObjectFactory.php | 8 +------ 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/src/Persistence/PersistenceManager.php b/src/Persistence/PersistenceManager.php index 70f6f9850..70872a4bf 100644 --- a/src/Persistence/PersistenceManager.php +++ b/src/Persistence/PersistenceManager.php @@ -186,7 +186,14 @@ public function refresh(object &$object, bool $force = false): object return $object; } - public function isPersisted(object $object): bool + /** + * @template T of object + * + * @param T $object + * + * @return ?T + */ + public function findPersisted(object $object): ?object { if ($object instanceof Proxy) { $object = unproxy($object); @@ -195,7 +202,16 @@ public function isPersisted(object $object): bool $om = $this->strategyFor($object::class)->objectManagerFor($object::class); $id = $om->getClassMetadata($object::class)->getIdentifierValues($object); - return $id && null !== $om->find($object::class, $id); + if (!$id) { + return null; + } + + return $om->find($object::class, $id); + } + + public function isPersisted(object $object): bool + { + return (bool) $this->findPersisted($object); } /** @@ -318,7 +334,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/PersistentObjectFactory.php b/src/Persistence/PersistentObjectFactory.php index 1167a95ab..d6a6ec198 100644 --- a/src/Persistence/PersistentObjectFactory.php +++ b/src/Persistence/PersistentObjectFactory.php @@ -12,7 +12,6 @@ namespace Zenstruck\Foundry\Persistence; use Doctrine\Persistence\ObjectRepository; -use Symfony\Component\VarExporter\Exception\LogicException as VarExportLogicException; use Zenstruck\Foundry\Configuration; use Zenstruck\Foundry\Exception\PersistenceDisabled; use Zenstruck\Foundry\Exception\PersistenceNotAvailable; @@ -20,7 +19,6 @@ use Zenstruck\Foundry\FactoryCollection; use Zenstruck\Foundry\ObjectFactory; use Zenstruck\Foundry\Persistence\Exception\NotEnoughObjects; -use Zenstruck\Foundry\Persistence\Exception\RefreshObjectFailed; use function Zenstruck\Foundry\set; @@ -359,11 +357,7 @@ protected function normalizeObject(object $object): object return $object; } - try { - return proxy($object)->_refresh()->_real(); - } catch (RefreshObjectFailed|VarExportLogicException) { - return $object; - } + return $configuration->persistence()->findPersisted($object) ?? $object; } final protected function isPersisting(): bool From c00b3f1d6a0029ac8800227255ff1eedfc44c7c6 Mon Sep 17 00:00:00 2001 From: Nicolas PHILIPPE Date: Thu, 2 Jan 2025 19:44:32 +0100 Subject: [PATCH 066/102] fix: isPersisted must work when id is known in advance (#774) --- composer.json | 3 +- src/Mongo/MongoPersistenceStrategy.php | 7 ++++ src/ORM/AbstractORMPersistenceStrategy.php | 5 +++ src/Persistence/PersistenceManager.php | 2 +- src/Persistence/PersistenceStrategy.php | 2 ++ tests/Fixture/Document/DocumentWithUid.php | 30 +++++++++++++++++ tests/Fixture/Entity/EntityWithUid.php | 25 ++++++++++++++ .../Mongo/PersistenceManagerTest.php | 26 +++++++++++++++ .../ORM/PersistenceManagerTest.php | 26 +++++++++++++++ .../PersistenceManagerTestCase.php | 33 +++++++++++++++++++ 10 files changed, 157 insertions(+), 2 deletions(-) create mode 100644 tests/Fixture/Document/DocumentWithUid.php create mode 100644 tests/Fixture/Entity/EntityWithUid.php create mode 100644 tests/Integration/Mongo/PersistenceManagerTest.php create mode 100644 tests/Integration/ORM/PersistenceManagerTest.php create mode 100644 tests/Integration/Persistence/PersistenceManagerTestCase.php diff --git a/composer.json b/composer.json index 73cf9ec6f..6b74cfb10 100644 --- a/composer.json +++ b/composer.json @@ -30,7 +30,7 @@ "brianium/paratest": "^6|^7", "dama/doctrine-test-bundle": "^7.0|^8.0", "doctrine/collections": "^1.7|^2.0", - "doctrine/common": "^3.2", + "doctrine/common": "^2|^3", "doctrine/doctrine-bundle": "^2.10", "doctrine/doctrine-migrations-bundle": "^2.2|^3.0", "doctrine/mongodb-odm-bundle": "^4.6|^5.0", @@ -42,6 +42,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" }, diff --git a/src/Mongo/MongoPersistenceStrategy.php b/src/Mongo/MongoPersistenceStrategy.php index f8dca2c09..4896d3f03 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; } + + final 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..221abd972 100644 --- a/src/ORM/AbstractORMPersistenceStrategy.php +++ b/src/ORM/AbstractORMPersistenceStrategy.php @@ -78,6 +78,11 @@ final public function isEmbeddable(object $object): bool return $this->objectManagerFor($object::class)->getClassMetadata($object::class)->isEmbeddedClass; } + final function isScheduledForInsert(object $object): bool + { + return $this->objectManagerFor($object::class)->getUnitOfWork()->isScheduledForInsert($object); + } + final public function managedNamespaces(): array { $namespaces = []; diff --git a/src/Persistence/PersistenceManager.php b/src/Persistence/PersistenceManager.php index 70872a4bf..ecee37f15 100644 --- a/src/Persistence/PersistenceManager.php +++ b/src/Persistence/PersistenceManager.php @@ -211,7 +211,7 @@ public function findPersisted(object $object): ?object public function isPersisted(object $object): bool { - return (bool) $this->findPersisted($object); + return !$this->strategyFor($object::class)->isScheduledForInsert($object) && $this->findPersisted($object); } /** diff --git a/src/Persistence/PersistenceStrategy.php b/src/Persistence/PersistenceStrategy.php index 13faaa7a0..85607ccde 100644 --- a/src/Persistence/PersistenceStrategy.php +++ b/src/Persistence/PersistenceStrategy.php @@ -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/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/EntityWithUid.php b/tests/Fixture/Entity/EntityWithUid.php new file mode 100644 index 000000000..cd920ad3a --- /dev/null +++ b/tests/Fixture/Entity/EntityWithUid.php @@ -0,0 +1,25 @@ + + */ +#[ORM\Entity] +class EntityWithUid +{ + #[ORM\Id] + #[ORM\Column(type: 'uuid')] + public Uuid $id; + + public function __construct() + { + $this->id = Uuid::v7(); + } +} diff --git a/tests/Integration/Mongo/PersistenceManagerTest.php b/tests/Integration/Mongo/PersistenceManagerTest.php new file mode 100644 index 000000000..305e9de6e --- /dev/null +++ b/tests/Integration/Mongo/PersistenceManagerTest.php @@ -0,0 +1,26 @@ +get(DocumentManager::class); // @phpstan-ignore return.type + } +} diff --git a/tests/Integration/ORM/PersistenceManagerTest.php b/tests/Integration/ORM/PersistenceManagerTest.php new file mode 100644 index 000000000..00fffeee6 --- /dev/null +++ b/tests/Integration/ORM/PersistenceManagerTest.php @@ -0,0 +1,26 @@ +get(EntityManagerInterface::class); // @phpstan-ignore return.type + } +} diff --git a/tests/Integration/Persistence/PersistenceManagerTestCase.php b/tests/Integration/Persistence/PersistenceManagerTestCase.php new file mode 100644 index 000000000..3f607b4df --- /dev/null +++ b/tests/Integration/Persistence/PersistenceManagerTestCase.php @@ -0,0 +1,33 @@ +createObject(); + + $this->objectManager()->persist($object); + + self::assertFalse( + Configuration::instance()->persistence()->isPersisted($object) + ); + } + + abstract protected static function createObject(): object; + + abstract protected static function objectManager(): ObjectManager; +} From 482fcdd7b8ec9a93418032990370116fb65723a2 Mon Sep 17 00:00:00 2001 From: nikophil Date: Thu, 2 Jan 2025 18:48:18 +0000 Subject: [PATCH 067/102] bot: fix cs [skip ci] --- src/Mongo/MongoPersistenceStrategy.php | 2 +- src/ORM/AbstractORMPersistenceStrategy.php | 2 +- tests/Fixture/Entity/EntityWithUid.php | 12 ++++++++++-- tests/Integration/Mongo/PersistenceManagerTest.php | 9 +++++++++ tests/Integration/ORM/PersistenceManagerTest.php | 11 ++++++++++- .../Persistence/PersistenceManagerTestCase.php | 9 +++++++++ 6 files changed, 40 insertions(+), 5 deletions(-) diff --git a/src/Mongo/MongoPersistenceStrategy.php b/src/Mongo/MongoPersistenceStrategy.php index 4896d3f03..f761c5a54 100644 --- a/src/Mongo/MongoPersistenceStrategy.php +++ b/src/Mongo/MongoPersistenceStrategy.php @@ -89,7 +89,7 @@ public function isEmbeddable(object $object): bool return $this->objectManagerFor($object::class)->getClassMetadata($object::class)->isEmbeddedDocument; } - final function isScheduledForInsert(object $object): bool + public function isScheduledForInsert(object $object): bool { $uow = $this->objectManagerFor($object::class)->getUnitOfWork(); diff --git a/src/ORM/AbstractORMPersistenceStrategy.php b/src/ORM/AbstractORMPersistenceStrategy.php index 221abd972..fbbb6c2f5 100644 --- a/src/ORM/AbstractORMPersistenceStrategy.php +++ b/src/ORM/AbstractORMPersistenceStrategy.php @@ -78,7 +78,7 @@ final public function isEmbeddable(object $object): bool return $this->objectManagerFor($object::class)->getClassMetadata($object::class)->isEmbeddedClass; } - final function isScheduledForInsert(object $object): bool + final public function isScheduledForInsert(object $object): bool { return $this->objectManagerFor($object::class)->getUnitOfWork()->isScheduledForInsert($object); } diff --git a/tests/Fixture/Entity/EntityWithUid.php b/tests/Fixture/Entity/EntityWithUid.php index cd920ad3a..3a695b5b5 100644 --- a/tests/Fixture/Entity/EntityWithUid.php +++ b/tests/Fixture/Entity/EntityWithUid.php @@ -2,11 +2,19 @@ declare(strict_types=1); +/* + * This file is part of the zenstruck/foundry package. + * + * (c) Kevin Bond + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Zenstruck\Foundry\Tests\Fixture\Entity; -use Symfony\Bridge\Doctrine\Types\UuidType; -use Symfony\Component\Uid\Uuid; use Doctrine\ORM\Mapping as ORM; +use Symfony\Component\Uid\Uuid; /** * @author Nicolas PHILIPPE diff --git a/tests/Integration/Mongo/PersistenceManagerTest.php b/tests/Integration/Mongo/PersistenceManagerTest.php index 305e9de6e..b1b87da9f 100644 --- a/tests/Integration/Mongo/PersistenceManagerTest.php +++ b/tests/Integration/Mongo/PersistenceManagerTest.php @@ -2,6 +2,15 @@ declare(strict_types=1); +/* + * This file is part of the zenstruck/foundry package. + * + * (c) Kevin Bond + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Zenstruck\Foundry\Tests\Integration\Mongo; use Doctrine\ODM\MongoDB\DocumentManager; diff --git a/tests/Integration/ORM/PersistenceManagerTest.php b/tests/Integration/ORM/PersistenceManagerTest.php index 00fffeee6..26258e356 100644 --- a/tests/Integration/ORM/PersistenceManagerTest.php +++ b/tests/Integration/ORM/PersistenceManagerTest.php @@ -2,12 +2,21 @@ declare(strict_types=1); +/* + * This file is part of the zenstruck/foundry package. + * + * (c) Kevin Bond + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Zenstruck\Foundry\Tests\Integration\ORM; use Doctrine\ORM\EntityManagerInterface; use Doctrine\Persistence\ObjectManager; -use Zenstruck\Foundry\Tests\Integration\Persistence\PersistenceManagerTestCase; 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 diff --git a/tests/Integration/Persistence/PersistenceManagerTestCase.php b/tests/Integration/Persistence/PersistenceManagerTestCase.php index 3f607b4df..f8d2e3992 100644 --- a/tests/Integration/Persistence/PersistenceManagerTestCase.php +++ b/tests/Integration/Persistence/PersistenceManagerTestCase.php @@ -2,6 +2,15 @@ declare(strict_types=1); +/* + * This file is part of the zenstruck/foundry package. + * + * (c) Kevin Bond + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Zenstruck\Foundry\Tests\Integration\Persistence; use Doctrine\Persistence\ObjectManager; From 29b48a1eca027f85450bacdce6ce74dbc0779c11 Mon Sep 17 00:00:00 2001 From: Nicolas PHILIPPE Date: Sat, 4 Jan 2025 14:53:50 +0100 Subject: [PATCH 068/102] test: add orphan removal premutation (#777) --- ...cadePersistOnLoadClassMetadataListener.php | 4 +++ ...hangesEntityRelationshipCascadePersist.php | 7 ++-- .../DoctrineCascadeRelationshipMetadata.php | 36 ++++++++++++++++--- 3 files changed, 41 insertions(+), 6 deletions(-) diff --git a/tests/Fixture/DoctrineCascadeRelationship/ChangeCascadePersistOnLoadClassMetadataListener.php b/tests/Fixture/DoctrineCascadeRelationship/ChangeCascadePersistOnLoadClassMetadataListener.php index e5b2e302b..ba3327ef9 100644 --- a/tests/Fixture/DoctrineCascadeRelationship/ChangeCascadePersistOnLoadClassMetadataListener.php +++ b/tests/Fixture/DoctrineCascadeRelationship/ChangeCascadePersistOnLoadClassMetadataListener.php @@ -44,6 +44,10 @@ public function loadClassMetadata(LoadClassMetadataEventArgs $eventArgs): void 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 index a244beec4..895747a0d 100644 --- a/tests/Fixture/DoctrineCascadeRelationship/ChangesEntityRelationshipCascadePersist.php +++ b/tests/Fixture/DoctrineCascadeRelationship/ChangesEntityRelationshipCascadePersist.php @@ -112,9 +112,12 @@ public static function provideCascadeRelationshipsCombinations(): iterable throw new \LogicException(\sprintf("Wrong parameters for attribute \"%s\". Association \"{$class}::\${$field}\" does not exist.", UsingRelationships::class)); } - $relationshipFields[] = ['class' => $association['sourceEntity'], 'field' => $association['fieldName']]; + $relationshipFields[] = ['class' => $association['sourceEntity'], 'field' => $association['fieldName'], 'isOneToMany' => $association['type'] === ClassMetadata::ONE_TO_MANY]; if ($association['inversedBy'] ?? $association['mappedBy'] ?? null) { - $relationshipFields[] = ['class' => $association['targetEntity'], 'field' => $association['inversedBy'] ?? $association['mappedBy']]; + /** @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' => $associationTargetEntity['type'] === ClassMetadata::ONE_TO_MANY]; } } } diff --git a/tests/Fixture/DoctrineCascadeRelationship/DoctrineCascadeRelationshipMetadata.php b/tests/Fixture/DoctrineCascadeRelationship/DoctrineCascadeRelationshipMetadata.php index ffc2be412..a5b3b54ed 100644 --- a/tests/Fixture/DoctrineCascadeRelationship/DoctrineCascadeRelationshipMetadata.php +++ b/tests/Fixture/DoctrineCascadeRelationship/DoctrineCascadeRelationshipMetadata.php @@ -22,24 +22,31 @@ private function __construct( public readonly string $class, public readonly string $field, public readonly bool $cascade, + public readonly bool $orphanRemoval, ) { } public function __toString(): string { - return \sprintf('%s::$%s - %s', $this->class, $this->field, $this->cascade ? 'cascade' : 'no cascade'); + $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): self + public static function fromArray(array $source, bool $cascade = false, bool $orphanRemoval = false): self { - return new self(class: $source['class'], field: $source['field'], cascade: $cascade); + return new self(class: $source['class'], field: $source['field'], cascade: $cascade, orphanRemoval: $orphanRemoval); } /** - * @param list $relationshipFields + * @param list $relationshipFields * @return \Generator> */ public static function allCombinations(array $relationshipFields): iterable @@ -55,6 +62,8 @@ public static function allCombinations(array $relationshipFields): iterable $total = 2 ** \count($relationshipFields); + $hasOneToMany = false; + for ($i = 0; $i < $total; ++$i) { $temp = []; @@ -64,9 +73,28 @@ public static function allCombinations(array $relationshipFields): iterable $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; } } From 8353bf0fc0c4b6842999ce55d2022b3b5373f973 Mon Sep 17 00:00:00 2001 From: nikophil Date: Sat, 4 Jan 2025 13:56:53 +0000 Subject: [PATCH 069/102] bot: fix cs [skip ci] --- .../ChangesEntityRelationshipCascadePersist.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Fixture/DoctrineCascadeRelationship/ChangesEntityRelationshipCascadePersist.php b/tests/Fixture/DoctrineCascadeRelationship/ChangesEntityRelationshipCascadePersist.php index 895747a0d..afeec36bb 100644 --- a/tests/Fixture/DoctrineCascadeRelationship/ChangesEntityRelationshipCascadePersist.php +++ b/tests/Fixture/DoctrineCascadeRelationship/ChangesEntityRelationshipCascadePersist.php @@ -112,12 +112,12 @@ public static function provideCascadeRelationshipsCombinations(): iterable 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' => $association['type'] === ClassMetadata::ONE_TO_MANY]; + $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' => $associationTargetEntity['type'] === ClassMetadata::ONE_TO_MANY]; + $relationshipFields[] = ['class' => $associationTargetEntity['sourceEntity'], 'field' => $associationTargetEntity['fieldName'], 'isOneToMany' => ClassMetadata::ONE_TO_MANY === $associationTargetEntity['type']]; } } } From 0d66c028db4ed79368b3651ac77dc04bf3c4129d Mon Sep 17 00:00:00 2001 From: Nicolas PHILIPPE Date: Sat, 4 Jan 2025 16:23:05 +0100 Subject: [PATCH 070/102] minor: use refresh for detached entities (#778) --- src/Persistence/PersistenceManager.php | 30 +++++++-------------- src/Persistence/PersistentObjectFactory.php | 9 ++++++- 2 files changed, 18 insertions(+), 21 deletions(-) diff --git a/src/Persistence/PersistenceManager.php b/src/Persistence/PersistenceManager.php index ecee37f15..3525791f9 100644 --- a/src/Persistence/PersistenceManager.php +++ b/src/Persistence/PersistenceManager.php @@ -155,8 +155,10 @@ public function refresh(object &$object, bool $force = false): object $strategy = $this->strategyFor($object::class); - if ($strategy->hasChanges($object)) { - throw RefreshObjectFailed::objectHasUnsavedChanges($object::class); + if (!$force) { + if ($strategy->hasChanges($object)) { + throw RefreshObjectFailed::objectHasUnsavedChanges($object::class); + } } $om = $strategy->objectManagerFor($object::class); @@ -186,15 +188,12 @@ public function refresh(object &$object, bool $force = false): object return $object; } - /** - * @template T of object - * - * @param T $object - * - * @return ?T - */ - public function findPersisted(object $object): ?object + public function isPersisted(object $object): bool { + if ($this->strategyFor($object::class)->isScheduledForInsert($object)) { + return false; + } + if ($object instanceof Proxy) { $object = unproxy($object); } @@ -202,16 +201,7 @@ public function findPersisted(object $object): ?object $om = $this->strategyFor($object::class)->objectManagerFor($object::class); $id = $om->getClassMetadata($object::class)->getIdentifierValues($object); - if (!$id) { - return null; - } - - return $om->find($object::class, $id); - } - - public function isPersisted(object $object): bool - { - return !$this->strategyFor($object::class)->isScheduledForInsert($object) && $this->findPersisted($object); + return $id && null !== $om->find($object::class, $id); } /** diff --git a/src/Persistence/PersistentObjectFactory.php b/src/Persistence/PersistentObjectFactory.php index d6a6ec198..d02a03cb3 100644 --- a/src/Persistence/PersistentObjectFactory.php +++ b/src/Persistence/PersistentObjectFactory.php @@ -12,6 +12,7 @@ namespace Zenstruck\Foundry\Persistence; use Doctrine\Persistence\ObjectRepository; +use Symfony\Component\VarExporter\Exception\LogicException as VarExportLogicException; use Zenstruck\Foundry\Configuration; use Zenstruck\Foundry\Exception\PersistenceDisabled; use Zenstruck\Foundry\Exception\PersistenceNotAvailable; @@ -20,6 +21,8 @@ use Zenstruck\Foundry\ObjectFactory; use Zenstruck\Foundry\Persistence\Exception\NotEnoughObjects; +use Zenstruck\Foundry\Persistence\Exception\RefreshObjectFailed; + use function Zenstruck\Foundry\set; /** @@ -357,7 +360,11 @@ protected function normalizeObject(object $object): object return $object; } - return $configuration->persistence()->findPersisted($object) ?? $object; + try { + return $configuration->persistence()->refresh($object, true); + } catch (RefreshObjectFailed|VarExportLogicException) { + return $object; + } } final protected function isPersisting(): bool From 3e9650a2367166d9180afe8f2ef25e0bd349a014 Mon Sep 17 00:00:00 2001 From: nikophil Date: Sat, 4 Jan 2025 15:26:05 +0000 Subject: [PATCH 071/102] bot: fix cs [skip ci] --- src/Persistence/PersistentObjectFactory.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Persistence/PersistentObjectFactory.php b/src/Persistence/PersistentObjectFactory.php index d02a03cb3..7e023d394 100644 --- a/src/Persistence/PersistentObjectFactory.php +++ b/src/Persistence/PersistentObjectFactory.php @@ -20,7 +20,6 @@ use Zenstruck\Foundry\FactoryCollection; use Zenstruck\Foundry\ObjectFactory; use Zenstruck\Foundry\Persistence\Exception\NotEnoughObjects; - use Zenstruck\Foundry\Persistence\Exception\RefreshObjectFailed; use function Zenstruck\Foundry\set; From 316d3c7637639dda86e94686f88cd0226b92bb16 Mon Sep 17 00:00:00 2001 From: Xavier Laviron Date: Wed, 8 Jan 2025 11:34:24 +0100 Subject: [PATCH 072/102] doc: fix typo (#782) --- docs/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.rst b/docs/index.rst index 03c13c141..06850a6e4 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1605,7 +1605,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. From 553807b3d48c28ae8ce887a69da33605400b9820 Mon Sep 17 00:00:00 2001 From: Kevin Bond Date: Wed, 15 Jan 2025 14:00:22 -0500 Subject: [PATCH 073/102] minor: add platform config to mysql docker container (#788) --- docker-compose.yaml | 1 + 1 file changed, 1 insertion(+) 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" From 200cfdd55eff55503db2efbe8f5fd59868160294 Mon Sep 17 00:00:00 2001 From: Javier Eguiluz Date: Thu, 16 Jan 2025 18:07:52 +0100 Subject: [PATCH 074/102] [Doc] Fix misc issues (#789) --- docs/index.rst | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index 06850a6e4..0a0aab2f3 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -14,7 +14,7 @@ or a combination of these. .. admonition:: Screencast :class: screencast - Want to watch a screencast πŸŽ₯ about it? Check out `https://symfonycasts.com/foundry`__ + Want to watch a screencast πŸŽ₯ about it? Check out `symfonycasts.com/foundry `_. .. warning:: @@ -534,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; @@ -1512,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 @@ -1521,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 ..................... @@ -1655,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. :: From 57c42bcdfd04ab1880bd88b5e7cbe6d205de9c0e Mon Sep 17 00:00:00 2001 From: Nicolas PHILIPPE Date: Sat, 18 Jan 2025 17:51:53 +0100 Subject: [PATCH 075/102] tests: fix a test after a bug was resolved in doctrine migrations (#791) --- .../Migration/ResetDatabaseWithMigrationTest.php | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/tests/Integration/Migration/ResetDatabaseWithMigrationTest.php b/tests/Integration/Migration/ResetDatabaseWithMigrationTest.php index eb07f3cc7..896e5c0ea 100644 --- a/tests/Integration/Migration/ResetDatabaseWithMigrationTest.php +++ b/tests/Integration/Migration/ResetDatabaseWithMigrationTest.php @@ -45,16 +45,9 @@ 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); + $exit = $application->run(new ArrayInput(['command' => 'doctrine:schema:validate', '-v' => true]), new BufferedOutput()); + + self::assertSame(0, $exit); } /** From d9262cc0288bceb0e5d1a8ef42db066da514f759 Mon Sep 17 00:00:00 2001 From: Nicolas PHILIPPE Date: Sat, 18 Jan 2025 19:37:10 +0100 Subject: [PATCH 076/102] fix: fix .gitattributes and `#[RequiresPhpUnit]` versions (#792) --- .gitattributes | 8 +++++++- .../Attribute/WithStory/WithStoryOnClassTest.php | 4 ++-- .../Attribute/WithStory/WithStoryOnMethodTest.php | 4 ++-- .../Attribute/WithStory/WithStoryOnParentClassTest.php | 4 ++-- .../DataProviderForServiceFactoryInKernelTestCaseTest.php | 4 ++-- tests/Integration/DataProvider/DataProviderInUnitTest.php | 4 ++-- ...ataProviderWithNonProxyFactoryInKernelTestCaseTest.php | 4 ++-- .../DataProviderWithProxyFactoryInKernelTestCase.php | 4 ++-- .../DataProvider/GenericDocumentProxyFactoryTest.php | 4 ++-- .../DataProvider/GenericEntityProxyFactoryTest.php | 4 ++-- tests/Integration/ORM/EdgeCasesRelationshipTest.php | 8 ++++---- .../EntityFactoryRelationshipTestCase.php | 4 ++-- .../PolymorphicEntityFactoryRelationshipTest.php | 4 ++-- .../ProxyEntityFactoryRelationshipTest.php | 4 ++-- .../StandardEntityFactoryRelationshipTest.php | 4 ++-- 15 files changed, 37 insertions(+), 31 deletions(-) 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/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 7ba7918a7..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 { 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/ORM/EdgeCasesRelationshipTest.php b/tests/Integration/ORM/EdgeCasesRelationshipTest.php index 1bf563f33..51deb4fa1 100644 --- a/tests/Integration/ORM/EdgeCasesRelationshipTest.php +++ b/tests/Integration/ORM/EdgeCasesRelationshipTest.php @@ -45,7 +45,7 @@ final class EdgeCasesRelationshipTest extends KernelTestCase #[Test] #[DataProvider('provideCascadeRelationshipsCombinations')] #[UsingRelationships(RelationshipWithGlobalEntity\RelationshipWithGlobalEntity::class, ['globalEntity'])] - #[RequiresPhpunit('^11.4')] + #[RequiresPhpunit('>=11.4')] public function it_can_use_flush_after_and_entity_from_global_state(): void { $relationshipWithGlobalEntityFactory = persistent_factory(RelationshipWithGlobalEntity\RelationshipWithGlobalEntity::class); @@ -72,7 +72,7 @@ public function it_can_use_flush_after_and_entity_from_global_state(): void #[Test] #[DataProvider('provideCascadeRelationshipsCombinations')] #[UsingRelationships(RichDomainMandatoryRelationship\OwningSide::class, ['main'])] - #[RequiresPhpunit('^11.4')] + #[RequiresPhpunit('>=11.4')] public function inversed_relationship_mandatory(): void { $owningSideEntityFactory = persistent_factory(RichDomainMandatoryRelationship\OwningSide::class); @@ -91,7 +91,7 @@ public function inversed_relationship_mandatory(): void #[Test] #[DataProvider('provideCascadeRelationshipsCombinations')] #[UsingRelationships(InversedOneToOneWithNonNullableOwning\OwningSide::class, ['inverseSide'])] - #[RequiresPhpunit('^11.4')] + #[RequiresPhpunit('>=11.4')] public function inverse_one_to_one_with_non_nullable_inverse_side(): void { $owningSideFactory = persistent_factory(InversedOneToOneWithNonNullableOwning\OwningSide::class); @@ -109,7 +109,7 @@ public function inverse_one_to_one_with_non_nullable_inverse_side(): void #[Test] #[DataProvider('provideCascadeRelationshipsCombinations')] #[UsingRelationships(InversedOneToOneWithSetter\OwningSide::class, ['inverseSide'])] - #[RequiresPhpunit('^11.4')] + #[RequiresPhpunit('>=11.4')] public function inverse_one_to_one_with_both_nullable(): void { $owningSideFactory = persistent_factory(InversedOneToOneWithSetter\OwningSide::class); diff --git a/tests/Integration/ORM/EntityRelationship/EntityFactoryRelationshipTestCase.php b/tests/Integration/ORM/EntityRelationship/EntityFactoryRelationshipTestCase.php index 4d004dba6..7b59e67f9 100644 --- a/tests/Integration/ORM/EntityRelationship/EntityFactoryRelationshipTestCase.php +++ b/tests/Integration/ORM/EntityRelationship/EntityFactoryRelationshipTestCase.php @@ -36,9 +36,9 @@ /** * @author Kevin Bond * @author Nicolas PHILIPPE - * @requires PHPUnit ^11.4 + * @requires PHPUnit >=11.4 */ -#[RequiresPhpunit('^11.4')] +#[RequiresPhpunit('>=11.4')] abstract class EntityFactoryRelationshipTestCase extends KernelTestCase { use ChangesEntityRelationshipCascadePersist, Factories, ResetDatabase; diff --git a/tests/Integration/ORM/EntityRelationship/PolymorphicEntityFactoryRelationshipTest.php b/tests/Integration/ORM/EntityRelationship/PolymorphicEntityFactoryRelationshipTest.php index 0eea999c5..7b85207fd 100644 --- a/tests/Integration/ORM/EntityRelationship/PolymorphicEntityFactoryRelationshipTest.php +++ b/tests/Integration/ORM/EntityRelationship/PolymorphicEntityFactoryRelationshipTest.php @@ -22,9 +22,9 @@ * * @author Kevin Bond * @author Nicolas PHILIPPE - * @requires PHPUnit ^11.4 + * @requires PHPUnit >=11.4 */ -#[RequiresPhpunit('^11.4')] +#[RequiresPhpunit('>=11.4')] final class PolymorphicEntityFactoryRelationshipTest extends EntityFactoryRelationshipTestCase { protected static function contactFactory(): ChildContactFactory diff --git a/tests/Integration/ORM/EntityRelationship/ProxyEntityFactoryRelationshipTest.php b/tests/Integration/ORM/EntityRelationship/ProxyEntityFactoryRelationshipTest.php index 98e4faa15..ebdaca361 100644 --- a/tests/Integration/ORM/EntityRelationship/ProxyEntityFactoryRelationshipTest.php +++ b/tests/Integration/ORM/EntityRelationship/ProxyEntityFactoryRelationshipTest.php @@ -31,9 +31,9 @@ /** * @author Kevin Bond * @author Nicolas PHILIPPE - * @requires PHPUnit ^11.4 + * @requires PHPUnit >=11.4 */ -#[RequiresPhpunit('^11.4')] +#[RequiresPhpunit('>=11.4')] final class ProxyEntityFactoryRelationshipTest extends EntityFactoryRelationshipTestCase { /** @test */ diff --git a/tests/Integration/ORM/EntityRelationship/StandardEntityFactoryRelationshipTest.php b/tests/Integration/ORM/EntityRelationship/StandardEntityFactoryRelationshipTest.php index 20095b9f5..f9b217cf8 100644 --- a/tests/Integration/ORM/EntityRelationship/StandardEntityFactoryRelationshipTest.php +++ b/tests/Integration/ORM/EntityRelationship/StandardEntityFactoryRelationshipTest.php @@ -22,9 +22,9 @@ /** * @author Kevin Bond * @author Nicolas PHILIPPE - * @requires PHPUnit ^11.4 + * @requires PHPUnit >=11.4 */ -#[RequiresPhpunit('^11.4')] +#[RequiresPhpunit('>=11.4')] final class StandardEntityFactoryRelationshipTest extends EntityFactoryRelationshipTestCase { protected static function contactFactory(): ContactFactory From e45913eb8f89dd137319751b7dbf30a80d8fc73a Mon Sep 17 00:00:00 2001 From: Nicolas PHILIPPE Date: Thu, 23 Jan 2025 16:51:18 +0100 Subject: [PATCH 077/102] fix: propagate "schedule for insert" to factory collection (#775) --- src/FactoryCollection.php | 30 ++++++++- src/Persistence/PersistMode.php | 2 +- src/Persistence/PersistenceManager.php | 6 +- src/Persistence/PersistentObjectFactory.php | 63 ++++++++++--------- .../InverseSide.php | 39 ++++++++++++ .../InversedOneToOneWithOneToMany/Item.php | 28 +++++++++ .../OwningSide.php | 63 +++++++++++++++++++ .../ORM/EdgeCasesRelationshipTest.php | 29 +++++++++ 8 files changed, 227 insertions(+), 33 deletions(-) create mode 100644 tests/Fixture/Entity/EdgeCases/InversedOneToOneWithOneToMany/InverseSide.php create mode 100644 tests/Fixture/Entity/EdgeCases/InversedOneToOneWithOneToMany/Item.php create mode 100644 tests/Fixture/Entity/EdgeCases/InversedOneToOneWithOneToMany/OwningSide.php diff --git a/src/FactoryCollection.php b/src/FactoryCollection.php index 6d8f20592..44bb31b99 100644 --- a/src/FactoryCollection.php +++ b/src/FactoryCollection.php @@ -11,6 +11,9 @@ namespace Zenstruck\Foundry; +use Zenstruck\Foundry\Persistence\PersistentObjectFactory; +use Zenstruck\Foundry\Persistence\PersistMode; + /** * @author Kevin Bond * @@ -22,12 +25,28 @@ */ final class FactoryCollection implements \IteratorAggregate { + private PersistMode $persistMode; + /** * @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; } /** @@ -133,7 +152,16 @@ public function all(): array $factories[] = $this->factory->with($attributesOrFactory)->with(['__index' => $i++]); } - return $factories; // @phpstan-ignore return.type (PHPStan does not understand we have an array of 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 diff --git a/src/Persistence/PersistMode.php b/src/Persistence/PersistMode.php index 69172cef7..21ff94a17 100644 --- a/src/Persistence/PersistMode.php +++ b/src/Persistence/PersistMode.php @@ -25,6 +25,6 @@ enum PersistMode public function isPersisting(): bool { - return self::PERSIST === $this; + return self::WITHOUT_PERSISTING !== $this; } } diff --git a/src/Persistence/PersistenceManager.php b/src/Persistence/PersistenceManager.php index 3525791f9..4f5072605 100644 --- a/src/Persistence/PersistenceManager.php +++ b/src/Persistence/PersistenceManager.php @@ -239,7 +239,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; + } } /** diff --git a/src/Persistence/PersistentObjectFactory.php b/src/Persistence/PersistentObjectFactory.php index 7e023d394..5e1f8710b 100644 --- a/src/Persistence/PersistentObjectFactory.php +++ b/src/Persistence/PersistentObjectFactory.php @@ -42,9 +42,6 @@ abstract class PersistentObjectFactory extends ObjectFactory /** @var list */ private array $tempAfterInstantiate = []; - /** @var list */ - private array $tempAfterPersist = []; - /** * @phpstan-param mixed|Parameters $criteriaOrId * @@ -207,7 +204,7 @@ public function create(callable|array $attributes = []): object $this->throwIfCannotCreateObject(); - if (!$this->isPersisting()) { + if ($this->persistMode() !== PersistMode::PERSIST) { return $object; } @@ -219,12 +216,6 @@ 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->normalizedParameters ?? throw new \LogicException('Factory::$normalizedParameters has not been initialized.'); @@ -254,6 +245,17 @@ final public function withoutPersisting(): static return $clone; } + /** + * @internal + */ + public function withPersistMode(PersistMode $persistMode): static + { + $clone = clone $this; + $clone->persist = $persistMode; + + return $clone; + } + /** * @phpstan-param callable(T, Parameters, static):void $callback */ @@ -272,11 +274,7 @@ protected function normalizeParameter(string $field, mixed $value): mixed } if ($value instanceof self && isset($this->persist)) { - $value = match ($this->persist) { - PersistMode::PERSIST => $value->andPersist(), - PersistMode::WITHOUT_PERSISTING => $value->withoutPersisting(), - PersistMode::NO_PERSIST_BUT_SCHEDULE_FOR_INSERT => $value->withoutPersistingButScheduleForInsert(), - }; + $value = $value->withPersistMode($this->persist); } if ($value instanceof self) { @@ -290,7 +288,7 @@ protected function normalizeParameter(string $field, mixed $value): mixed // 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->withoutPersistingButScheduleForInsert() + $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 @@ -325,9 +323,9 @@ protected function normalizeCollection(string $field, FactoryCollection $collect if ($inverseRelationshipMetadata && $inverseRelationshipMetadata->isCollection) { $inverseField = $inverseRelationshipMetadata->inverseField; - $this->tempAfterPersist[] = static function(object $object) use ($collection, $inverseField, $pm) { - $collection->create([$inverseField => $object]); - $pm->refresh($object); + $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 @@ -374,19 +372,32 @@ final protected function isPersisting(): bool return false; } - $persistMode = $this->persist ?? ($config->persistence()->autoPersist(static::class()) ? PersistMode::PERSIST : PersistMode::WITHOUT_PERSISTING); + return $this->persistMode()->isPersisting(); + } - return $persistMode->isPersisting(); + /** + * @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); } /** * 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() && (!isset($factory->persist) || PersistMode::NO_PERSIST_BUT_SCHEDULE_FOR_INSERT !== $factory->persist)) { + if (!$factory->isPersisting()) { return; } @@ -395,14 +406,6 @@ static function(object $object, array $parameters, PersistentObjectFactory $fact ); } - private function withoutPersistingButScheduleForInsert(): static - { - $clone = clone $this; - $clone->persist = PersistMode::NO_PERSIST_BUT_SCHEDULE_FOR_INSERT; - - return $clone; - } - private function throwIfCannotCreateObject(): void { $configuration = Configuration::instance(); 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/InversedOneToOneWithOneToMany/Item.php b/tests/Fixture/Entity/EdgeCases/InversedOneToOneWithOneToMany/Item.php new file mode 100644 index 000000000..dfdf2aca6 --- /dev/null +++ b/tests/Fixture/Entity/EdgeCases/InversedOneToOneWithOneToMany/Item.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Tests\Fixture\Entity\EdgeCases\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_item_if_collection')] +class Item extends Base +{ + #[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/Integration/ORM/EdgeCasesRelationshipTest.php b/tests/Integration/ORM/EdgeCasesRelationshipTest.php index 51deb4fa1..65b425b49 100644 --- a/tests/Integration/ORM/EdgeCasesRelationshipTest.php +++ b/tests/Integration/ORM/EdgeCasesRelationshipTest.php @@ -22,6 +22,7 @@ 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; @@ -123,6 +124,34 @@ public function inverse_one_to_one_with_both_nullable(): void 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 */ From ee26af3e36f56eb5ba002f6688870968a9d562f3 Mon Sep 17 00:00:00 2001 From: nikophil <10139766+nikophil@users.noreply.github.com> Date: Thu, 23 Jan 2025 15:54:33 +0000 Subject: [PATCH 078/102] bot: fix cs [skip ci] --- src/FactoryCollection.php | 4 +-- src/Persistence/PersistentObjectFactory.php | 30 +++++++++---------- .../ORM/EdgeCasesRelationshipTest.php | 2 +- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/FactoryCollection.php b/src/FactoryCollection.php index 44bb31b99..b2319f909 100644 --- a/src/FactoryCollection.php +++ b/src/FactoryCollection.php @@ -152,8 +152,8 @@ public function all(): array $factories[] = $this->factory->with($attributesOrFactory)->with(['__index' => $i++]); } - return array_map( // @phpstan-ignore return.type (PHPStan does not understand we have an array of factories) - function (Factory $f) { + 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); } diff --git a/src/Persistence/PersistentObjectFactory.php b/src/Persistence/PersistentObjectFactory.php index 5e1f8710b..6649d04a3 100644 --- a/src/Persistence/PersistentObjectFactory.php +++ b/src/Persistence/PersistentObjectFactory.php @@ -204,7 +204,7 @@ public function create(callable|array $attributes = []): object $this->throwIfCannotCreateObject(); - if ($this->persistMode() !== PersistMode::PERSIST) { + if (PersistMode::PERSIST !== $this->persistMode()) { return $object; } @@ -267,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()) { @@ -375,20 +389,6 @@ final protected function isPersisting(): bool return $this->persistMode()->isPersisting(); } - /** - * @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); - } - /** * Schedule any new object for insert right after instantiation. * @internal diff --git a/tests/Integration/ORM/EdgeCasesRelationshipTest.php b/tests/Integration/ORM/EdgeCasesRelationshipTest.php index 65b425b49..3320e6b86 100644 --- a/tests/Integration/ORM/EdgeCasesRelationshipTest.php +++ b/tests/Integration/ORM/EdgeCasesRelationshipTest.php @@ -141,7 +141,7 @@ public function inverse_one_to_one_with_one_to_many(): void $inverseSide = $inverseSideFactory->create([ 'owningSide' => $owningSideFactory->with([ 'items' => $itemFactory->many(2), - ]) + ]), ]); $owningSideFactory::assert()->count(1); From 17388bc3f6fd465bc09dc37003e1f8dbaf67f3d8 Mon Sep 17 00:00:00 2001 From: Nicolas PHILIPPE Date: Thu, 23 Jan 2025 18:19:24 +0100 Subject: [PATCH 079/102] tests: transform "migrate" testsuite into "reset database" testsuite (#763) --- .github/workflows/ci.yml | 33 ++-- .gitignore | 1 - README.md | 8 +- composer.json | 12 +- phpunit | 2 +- phpunit-10.xml.dist | 6 +- phpunit-paratest.xml.dist | 2 +- phpunit.xml.dist | 6 +- .../ResetDatabase/SchemaDatabaseResetter.php | 2 +- .../Fixture/EntityInAnotherSchema/Article.php | 2 +- tests/Fixture/FoundryTestKernel.php | 162 ++++++++++++++++++ .../MigrationTests/TestMigrationKernel.php | 119 ------------- .../ResetDatabase/ResetDatabaseTestKernel.php | 68 ++++++++ .../migration-configuration-transactional.php | 2 +- .../migration-configuration.php | 2 +- tests/Fixture/TestKernel.php | 129 +------------- .../ResetDatabaseWithMigrationTest.php | 99 ----------- .../ORM/EdgeCasesRelationshipTest.php | 28 --- tests/Integration/Persistence/StoryTest.php | 43 ----- .../ResetDatabase/GlobalStoryTest.php | 45 +++++ .../ResetDatabase/OrmEdgeCaseTest.php | 49 ++++++ .../ResetDatabase/ResetDatabaseTest.php | 101 +++++++++++ .../ResetDatabase/ResetDatabaseTestCase.php | 20 +++ tests/bootstrap-migrate.php | 45 ----- tests/bootstrap-reset-database.php | 47 +++++ 25 files changed, 539 insertions(+), 494 deletions(-) create mode 100644 tests/Fixture/FoundryTestKernel.php delete mode 100644 tests/Fixture/MigrationTests/TestMigrationKernel.php create mode 100644 tests/Fixture/ResetDatabase/ResetDatabaseTestKernel.php rename tests/Fixture/{MigrationTests/configs => ResetDatabase/migration-configs}/migration-configuration-transactional.php (74%) rename tests/Fixture/{MigrationTests/configs => ResetDatabase/migration-configs}/migration-configuration.php (73%) delete mode 100644 tests/Integration/Migration/ResetDatabaseWithMigrationTest.php create mode 100644 tests/Integration/ResetDatabase/GlobalStoryTest.php create mode 100644 tests/Integration/ResetDatabase/OrmEdgeCaseTest.php create mode 100644 tests/Integration/ResetDatabase/ResetDatabaseTest.php create mode 100644 tests/Integration/ResetDatabase/ResetDatabaseTestCase.php delete mode 100644 tests/bootstrap-migrate.php create mode 100644 tests/bootstrap-reset-database.php diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 954bf3061..c0677bd28 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -89,28 +89,33 @@ 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: Test reset database - 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)' || '' }} 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' + reset-database-mode: [ schema, migrate ] + migration-configuration-file: ['no', 'migration-configuration', 'migration-configuration-transactional'] + include: + - { database: mongo, migration-configuration-file: 'no', use-dama: 0, reset-database-mode: schema } 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' + migration-configuration-file: 'migration-configuration-transactional' + - reset-database-mode: schema + migration-configuration-file: 'migration-configuration' + - reset-database-mode: schema + migration-configuration-file: 'migration-configuration-transactional' 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 +130,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 @@ -149,7 +158,7 @@ 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 shell: bash test-with-paratest: diff --git a/.gitignore b/.gitignore index 6d5aba94f..5ce100385 100644 --- a/.gitignore +++ b/.gitignore @@ -14,5 +14,4 @@ /docker/.makefile /.env.local /docker-compose.override.yaml -/tests/Fixture/MigrationTests/Migrations/ /tests/Fixture/Maker/tmp/ 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/composer.json b/composer.json index 6b74cfb10..0b55de8b3 100644 --- a/composer.json +++ b/composer.json @@ -86,15 +86,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/phpunit b/phpunit index 01c97cf36..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 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 387352e86..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 diff --git a/phpunit.xml.dist b/phpunit.xml.dist index c6b9f26cb..95f1c3e92 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -19,10 +19,10 @@ tests - tests/Integration/Migration/ResetDatabaseWithMigrationTest.php + tests/Integration/ResetDatabase - - tests/Integration/Migration/ResetDatabaseWithMigrationTest.php + + tests/Integration/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/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/FoundryTestKernel.php b/tests/Fixture/FoundryTestKernel.php new file mode 100644 index 000000000..681669131 --- /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(); + } + } + + 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); + } + + 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 \getenv('DATABASE_RESET_MODE') === 'migrate'; + } + + public static function usesDamaDoctrineTestBundle(): bool + { + return (bool)\getenv('USE_DAMA_DOCTRINE_TEST_BUNDLE'); + } +} 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/ResetDatabase/ResetDatabaseTestKernel.php b/tests/Fixture/ResetDatabase/ResetDatabaseTestKernel.php new file mode 100644 index 000000000..aebbd49ce --- /dev/null +++ b/tests/Fixture/ResetDatabase/ResetDatabaseTestKernel.php @@ -0,0 +1,68 @@ + + * + * 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); + } +} diff --git a/tests/Fixture/MigrationTests/configs/migration-configuration-transactional.php b/tests/Fixture/ResetDatabase/migration-configs/migration-configuration-transactional.php similarity index 74% rename from tests/Fixture/MigrationTests/configs/migration-configuration-transactional.php rename to tests/Fixture/ResetDatabase/migration-configs/migration-configuration-transactional.php index cb0928594..c8f5232f7 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__).'/Migrations', ], 'transactional' => true, ]; diff --git a/tests/Fixture/MigrationTests/configs/migration-configuration.php b/tests/Fixture/ResetDatabase/migration-configs/migration-configuration.php similarity index 73% rename from tests/Fixture/MigrationTests/configs/migration-configuration.php rename to tests/Fixture/ResetDatabase/migration-configs/migration-configuration.php index def58e6f5..b897fc47c 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/Migrations', ], 'transactional' => false, ]; diff --git a/tests/Fixture/TestKernel.php b/tests/Fixture/TestKernel.php index 50ae9529c..edb4ddfa5 100644 --- a/tests/Fixture/TestKernel.php +++ b/tests/Fixture/TestKernel.php @@ -11,68 +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\Persistence\PersistenceManager; -use Zenstruck\Foundry\Tests\Fixture\DoctrineCascadeRelationship\ChangeCascadePersistOnLoadClassMetadataListener; 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, @@ -80,92 +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], - ], - ]); - - $c->register(ChangeCascadePersistOnLoadClassMetadataListener::class) - ->setAutowired(true) - ->setAutoconfigured(true); - $c->setAlias(PersistenceManager::class, '.zenstruck_foundry.persistence_manager') - ->setPublic(true); - } - - 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/Migration/ResetDatabaseWithMigrationTest.php b/tests/Integration/Migration/ResetDatabaseWithMigrationTest.php deleted file mode 100644 index 896e5c0ea..000000000 --- a/tests/Integration/Migration/ResetDatabaseWithMigrationTest.php +++ /dev/null @@ -1,99 +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\ContactFactory; -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]), new BufferedOutput()); - - self::assertSame(0, $exit); - } - - /** - * @test - */ - public function it_can_store_object(): void - { - ContactFactory::assert()->count(0); - - ContactFactory::createOne(); - - ContactFactory::assert()->count(1); - } - - /** - * @test - * @depends it_can_store_object - */ - public function it_starts_from_fresh_db(): void - { - ContactFactory::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/ORM/EdgeCasesRelationshipTest.php b/tests/Integration/ORM/EdgeCasesRelationshipTest.php index 3320e6b86..3db2c7c97 100644 --- a/tests/Integration/ORM/EdgeCasesRelationshipTest.php +++ b/tests/Integration/ORM/EdgeCasesRelationshipTest.php @@ -32,7 +32,6 @@ 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; /** @@ -42,33 +41,6 @@ final class EdgeCasesRelationshipTest extends KernelTestCase { use ChangesEntityRelationshipCascadePersist, Factories, RequiresORM, ResetDatabase; - /** @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()); - } - /** @test */ #[Test] #[DataProvider('provideCascadeRelationshipsCombinations')] 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..63c25e99b --- /dev/null +++ b/tests/Integration/ResetDatabase/GlobalStoryTest.php @@ -0,0 +1,45 @@ +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..6dc247b1f --- /dev/null +++ b/tests/Integration/ResetDatabase/OrmEdgeCaseTest.php @@ -0,0 +1,49 @@ +=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..52116d407 --- /dev/null +++ b/tests/Integration/ResetDatabase/ResetDatabaseTest.php @@ -0,0 +1,101 @@ + + * + * 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\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 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); + } +} diff --git a/tests/Integration/ResetDatabase/ResetDatabaseTestCase.php b/tests/Integration/ResetDatabase/ResetDatabaseTestCase.php new file mode 100644 index 000000000..81b04930b --- /dev/null +++ b/tests/Integration/ResetDatabase/ResetDatabaseTestCase.php @@ -0,0 +1,20 @@ + - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -use 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..d876245a3 --- /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'); + +(new Dotenv())->usePutenv()->loadEnv(__DIR__.'/../.env'); + +if (FoundryTestKernel::usesMigrations()) { + $fs->mkdir(__DIR__ . '/../var/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(); +} From 73abb8011cb1165859774bcb5889e85e41002bbf Mon Sep 17 00:00:00 2001 From: nikophil <10139766+nikophil@users.noreply.github.com> Date: Thu, 23 Jan 2025 17:24:24 +0000 Subject: [PATCH 080/102] bot: fix cs [skip ci] --- tests/Fixture/FoundryTestKernel.php | 40 +++++++++---------- .../ResetDatabase/ResetDatabaseTestKernel.php | 5 +-- .../ORM/EdgeCasesRelationshipTest.php | 3 -- .../ResetDatabase/GlobalStoryTest.php | 9 +++++ .../ResetDatabase/OrmEdgeCaseTest.php | 11 ++++- .../ResetDatabase/ResetDatabaseTestCase.php | 9 +++++ tests/bootstrap-reset-database.php | 4 +- 7 files changed, 52 insertions(+), 29 deletions(-) diff --git a/tests/Fixture/FoundryTestKernel.php b/tests/Fixture/FoundryTestKernel.php index 681669131..a649dff4e 100644 --- a/tests/Fixture/FoundryTestKernel.php +++ b/tests/Fixture/FoundryTestKernel.php @@ -50,6 +50,26 @@ public function registerBundles(): iterable } } + 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', [ @@ -139,24 +159,4 @@ protected function configureContainer(ContainerBuilder $c, LoaderInterface $load $c->register('logger', NullLogger::class); } - - 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 \getenv('DATABASE_RESET_MODE') === 'migrate'; - } - - public static function usesDamaDoctrineTestBundle(): bool - { - return (bool)\getenv('USE_DAMA_DOCTRINE_TEST_BUNDLE'); - } } diff --git a/tests/Fixture/ResetDatabase/ResetDatabaseTestKernel.php b/tests/Fixture/ResetDatabase/ResetDatabaseTestKernel.php index aebbd49ce..60e4139ca 100644 --- a/tests/Fixture/ResetDatabase/ResetDatabaseTestKernel.php +++ b/tests/Fixture/ResetDatabase/ResetDatabaseTestKernel.php @@ -43,8 +43,7 @@ protected function configureContainer(ContainerBuilder $c, LoaderInterface $load GlobalInvokableService::class, ], 'orm' => [ - 'reset' => - FoundryTestKernel::usesMigrations() + 'reset' => FoundryTestKernel::usesMigrations() ? [ 'mode' => ResetDatabaseMode::MIGRATE, 'migrations' => [ @@ -59,7 +58,7 @@ protected function configureContainer(ContainerBuilder $c, LoaderInterface $load // 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' + include __DIR__.'/migration-configs/migration-configuration.php' ); } diff --git a/tests/Integration/ORM/EdgeCasesRelationshipTest.php b/tests/Integration/ORM/EdgeCasesRelationshipTest.php index 3db2c7c97..27a94e13c 100644 --- a/tests/Integration/ORM/EdgeCasesRelationshipTest.php +++ b/tests/Integration/ORM/EdgeCasesRelationshipTest.php @@ -25,11 +25,8 @@ 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\persistent_factory; diff --git a/tests/Integration/ResetDatabase/GlobalStoryTest.php b/tests/Integration/ResetDatabase/GlobalStoryTest.php index 63c25e99b..c6d88124d 100644 --- a/tests/Integration/ResetDatabase/GlobalStoryTest.php +++ b/tests/Integration/ResetDatabase/GlobalStoryTest.php @@ -2,6 +2,15 @@ declare(strict_types=1); +/* + * This file is part of the zenstruck/foundry package. + * + * (c) Kevin Bond + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Zenstruck\Foundry\Tests\Integration\ResetDatabase; use Zenstruck\Foundry\Tests\Fixture\Document\GlobalDocument; diff --git a/tests/Integration/ResetDatabase/OrmEdgeCaseTest.php b/tests/Integration/ResetDatabase/OrmEdgeCaseTest.php index 6dc247b1f..4cae04a76 100644 --- a/tests/Integration/ResetDatabase/OrmEdgeCaseTest.php +++ b/tests/Integration/ResetDatabase/OrmEdgeCaseTest.php @@ -2,6 +2,15 @@ declare(strict_types=1); +/* + * This file is part of the zenstruck/foundry package. + * + * (c) Kevin Bond + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Zenstruck\Foundry\Tests\Integration\ResetDatabase; use PHPUnit\Framework\Attributes\DataProvider; @@ -9,9 +18,9 @@ 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 Zenstruck\Foundry\Tests\Fixture\Entity\EdgeCases\RelationshipWithGlobalEntity; use function Zenstruck\Foundry\Persistence\flush_after; use function Zenstruck\Foundry\Persistence\persistent_factory; diff --git a/tests/Integration/ResetDatabase/ResetDatabaseTestCase.php b/tests/Integration/ResetDatabase/ResetDatabaseTestCase.php index 81b04930b..f09625cab 100644 --- a/tests/Integration/ResetDatabase/ResetDatabaseTestCase.php +++ b/tests/Integration/ResetDatabase/ResetDatabaseTestCase.php @@ -2,6 +2,15 @@ declare(strict_types=1); +/* + * This file is part of the zenstruck/foundry package. + * + * (c) Kevin Bond + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Zenstruck\Foundry\Tests\Integration\ResetDatabase; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; diff --git a/tests/bootstrap-reset-database.php b/tests/bootstrap-reset-database.php index d876245a3..23edea0ca 100644 --- a/tests/bootstrap-reset-database.php +++ b/tests/bootstrap-reset-database.php @@ -26,7 +26,7 @@ (new Dotenv())->usePutenv()->loadEnv(__DIR__.'/../.env'); if (FoundryTestKernel::usesMigrations()) { - $fs->mkdir(__DIR__ . '/../var/Migrations'); + $fs->mkdir(__DIR__.'/../var/Migrations'); $kernel = new ResetDatabaseTestKernel('test', true); $kernel->boot(); @@ -38,7 +38,7 @@ $configuration = ''; if (\getenv('MIGRATION_CONFIGURATION_FILE')) { - $configuration = '--configuration ' . \getcwd() . '/' . \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); From bd50f4103899decc00cea9117c92113cdd257cce Mon Sep 17 00:00:00 2001 From: Nicolas PHILIPPE Date: Thu, 23 Jan 2025 18:31:15 +0100 Subject: [PATCH 081/102] fix: add unpersisted object to relation (#780) --- src/Persistence/PersistenceManager.php | 1 + src/Persistence/PersistentObjectFactory.php | 21 +++++++++- .../EntityFactoryRelationshipTestCase.php | 38 +++++++++++++++++++ 3 files changed, 58 insertions(+), 2 deletions(-) diff --git a/src/Persistence/PersistenceManager.php b/src/Persistence/PersistenceManager.php index 4f5072605..79730a6e5 100644 --- a/src/Persistence/PersistenceManager.php +++ b/src/Persistence/PersistenceManager.php @@ -190,6 +190,7 @@ public function refresh(object &$object, bool $force = false): 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; } diff --git a/src/Persistence/PersistentObjectFactory.php b/src/Persistence/PersistentObjectFactory.php index 6649d04a3..383e8f28b 100644 --- a/src/Persistence/PersistentObjectFactory.php +++ b/src/Persistence/PersistentObjectFactory.php @@ -367,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 $configuration->persistence()->refresh($object, true); + return $persistenceManager->refresh($object, force: true); } catch (RefreshObjectFailed|VarExportLogicException) { return $object; } diff --git a/tests/Integration/ORM/EntityRelationship/EntityFactoryRelationshipTestCase.php b/tests/Integration/ORM/EntityRelationship/EntityFactoryRelationshipTestCase.php index 7b59e67f9..dad3d1b34 100644 --- a/tests/Integration/ORM/EntityRelationship/EntityFactoryRelationshipTestCase.php +++ b/tests/Integration/ORM/EntityRelationship/EntityFactoryRelationshipTestCase.php @@ -31,6 +31,7 @@ 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; /** @@ -361,6 +362,43 @@ public function ensure_one_to_many_relations_are_not_pre_persisted(): void } } + /** @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() + ); + } + + 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()); + } + /** @return PersistentObjectFactory */ protected static function contactFactoryWithoutCategory(): PersistentObjectFactory { From 8dcda09ff622393a55ce6d17807ca716d99ab145 Mon Sep 17 00:00:00 2001 From: nikophil <10139766+nikophil@users.noreply.github.com> Date: Thu, 23 Jan 2025 17:41:41 +0000 Subject: [PATCH 082/102] bot: fix cs [skip ci] --- .../EntityFactoryRelationshipTestCase.php | 31 +++++++++---------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/tests/Integration/ORM/EntityRelationship/EntityFactoryRelationshipTestCase.php b/tests/Integration/ORM/EntityRelationship/EntityFactoryRelationshipTestCase.php index dad3d1b34..c949adc74 100644 --- a/tests/Integration/ORM/EntityRelationship/EntityFactoryRelationshipTestCase.php +++ b/tests/Integration/ORM/EntityRelationship/EntityFactoryRelationshipTestCase.php @@ -31,7 +31,6 @@ 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; /** @@ -384,21 +383,6 @@ public function it_can_add_unmanaged_entity_to_many_to_one(): void ); } - 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()); - } - /** @return PersistentObjectFactory */ protected static function contactFactoryWithoutCategory(): PersistentObjectFactory { @@ -417,6 +401,21 @@ 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 */ From 884113f20f968909fd2ef06e239814bf202708e5 Mon Sep 17 00:00:00 2001 From: Nicolas PHILIPPE Date: Thu, 23 Jan 2025 18:43:37 +0100 Subject: [PATCH 083/102] fix: simplify reset database extension (#779) --- config/mongo.php | 4 +- config/orm.php | 20 ++----- src/ZenstruckFoundryBundle.php | 29 +++++----- .../ResetDatabase/MongoResetterDecorator.php | 27 +++++++++ .../ResetDatabase/OrmResetterDecorator.php | 40 +++++++++++++ .../ResetDatabase/ResetDatabaseTestKernel.php | 8 +++ .../ResetDatabase/ResetDatabaseTest.php | 56 +++++++++++++++++++ 7 files changed, 154 insertions(+), 30 deletions(-) create mode 100644 tests/Fixture/ResetDatabase/MongoResetterDecorator.php create mode 100644 tests/Fixture/ResetDatabase/OrmResetterDecorator.php 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/src/ZenstruckFoundryBundle.php b/src/ZenstruckFoundryBundle.php index 21dc84459..87df05c05 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 ($resetMode === ResetDatabaseMode::MIGRATE) { + $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/tests/Fixture/ResetDatabase/MongoResetterDecorator.php b/tests/Fixture/ResetDatabase/MongoResetterDecorator.php new file mode 100644 index 000000000..25baa5395 --- /dev/null +++ b/tests/Fixture/ResetDatabase/MongoResetterDecorator.php @@ -0,0 +1,27 @@ +decorated->resetBeforeEachTest($kernel); + } +} diff --git a/tests/Fixture/ResetDatabase/OrmResetterDecorator.php b/tests/Fixture/ResetDatabase/OrmResetterDecorator.php new file mode 100644 index 000000000..ff069737a --- /dev/null +++ b/tests/Fixture/ResetDatabase/OrmResetterDecorator.php @@ -0,0 +1,40 @@ +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 index 60e4139ca..f4d28b26f 100644 --- a/tests/Fixture/ResetDatabase/ResetDatabaseTestKernel.php +++ b/tests/Fixture/ResetDatabase/ResetDatabaseTestKernel.php @@ -63,5 +63,13 @@ protected function configureContainer(ContainerBuilder $c, LoaderInterface $load } $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/Integration/ResetDatabase/ResetDatabaseTest.php b/tests/Integration/ResetDatabase/ResetDatabaseTest.php index 52116d407..e3fc34e87 100644 --- a/tests/Integration/ResetDatabase/ResetDatabaseTest.php +++ b/tests/Integration/ResetDatabase/ResetDatabaseTest.php @@ -17,7 +17,10 @@ use Symfony\Component\Console\Input\ArrayInput; use Symfony\Component\Console\Output\BufferedOutput; use Zenstruck\Foundry\Tests\Fixture\EntityInAnotherSchema\Article; +use Zenstruck\Foundry\Persistence\PersistenceManager; use Zenstruck\Foundry\Tests\Fixture\Factories\Document\GenericDocumentFactory; +use Zenstruck\Foundry\Tests\Fixture\ResetDatabase\MongoResetterDecorator; +use Zenstruck\Foundry\Tests\Fixture\ResetDatabase\OrmResetterDecorator; use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\GenericEntityFactory; use Zenstruck\Foundry\Tests\Fixture\FoundryTestKernel; @@ -98,4 +101,57 @@ public function can_create_object_in_another_schema(): void 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); + } } From 9937b115654270a26265a2f71cda0c509032b8ba Mon Sep 17 00:00:00 2001 From: Nicolas PHILIPPE Date: Thu, 23 Jan 2025 18:45:06 +0100 Subject: [PATCH 084/102] chore: add issue template (#795) --- .github/issue_template.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 .github/issue_template.md 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 | From a186c96aa50aee45805553fcb73fda9a4ee0e511 Mon Sep 17 00:00:00 2001 From: nikophil <10139766+nikophil@users.noreply.github.com> Date: Thu, 23 Jan 2025 17:48:00 +0000 Subject: [PATCH 085/102] bot: fix cs [skip ci] --- src/ZenstruckFoundryBundle.php | 2 +- .../ResetDatabase/MongoResetterDecorator.php | 11 ++++++++++- .../Fixture/ResetDatabase/OrmResetterDecorator.php | 14 ++++++++++++-- .../ResetDatabase/ResetDatabaseTest.php | 6 +++--- 4 files changed, 26 insertions(+), 7 deletions(-) diff --git a/src/ZenstruckFoundryBundle.php b/src/ZenstruckFoundryBundle.php index 87df05c05..e8e188be6 100644 --- a/src/ZenstruckFoundryBundle.php +++ b/src/ZenstruckFoundryBundle.php @@ -261,7 +261,7 @@ public function loadExtension(array $config, ContainerConfigurator $configurator } ); - if ($resetMode === ResetDatabaseMode::MIGRATE) { + if (ResetDatabaseMode::MIGRATE === $resetMode) { $container->getDefinition(OrmResetter::class) ->replaceArgument('$configurations', $config['orm']['reset']['migrations']['configurations']) ; diff --git a/tests/Fixture/ResetDatabase/MongoResetterDecorator.php b/tests/Fixture/ResetDatabase/MongoResetterDecorator.php index 25baa5395..05ab0a822 100644 --- a/tests/Fixture/ResetDatabase/MongoResetterDecorator.php +++ b/tests/Fixture/ResetDatabase/MongoResetterDecorator.php @@ -2,6 +2,15 @@ declare(strict_types=1); +/* + * This file is part of the zenstruck/foundry package. + * + * (c) Kevin Bond + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Zenstruck\Foundry\Tests\Fixture\ResetDatabase; use Symfony\Component\DependencyInjection\Attribute\AsDecorator; @@ -14,7 +23,7 @@ final class MongoResetterDecorator implements MongoResetter public static bool $calledBeforeEachTest = false; public function __construct( - private MongoResetter $decorated + private MongoResetter $decorated, ) { } diff --git a/tests/Fixture/ResetDatabase/OrmResetterDecorator.php b/tests/Fixture/ResetDatabase/OrmResetterDecorator.php index ff069737a..0bd836458 100644 --- a/tests/Fixture/ResetDatabase/OrmResetterDecorator.php +++ b/tests/Fixture/ResetDatabase/OrmResetterDecorator.php @@ -2,6 +2,15 @@ declare(strict_types=1); +/* + * This file is part of the zenstruck/foundry package. + * + * (c) Kevin Bond + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Zenstruck\Foundry\Tests\Fixture\ResetDatabase; use Symfony\Component\DependencyInjection\Attribute\AsDecorator; @@ -15,8 +24,9 @@ final class OrmResetterDecorator implements OrmResetter public static bool $calledBeforeEachTest = false; public function __construct( - private OrmResetter $decorated - ) {} + private OrmResetter $decorated, + ) { + } public function resetBeforeFirstTest(KernelInterface $kernel): void { diff --git a/tests/Integration/ResetDatabase/ResetDatabaseTest.php b/tests/Integration/ResetDatabase/ResetDatabaseTest.php index e3fc34e87..304a83e09 100644 --- a/tests/Integration/ResetDatabase/ResetDatabaseTest.php +++ b/tests/Integration/ResetDatabase/ResetDatabaseTest.php @@ -16,13 +16,13 @@ use Symfony\Bundle\FrameworkBundle\Console\Application; use Symfony\Component\Console\Input\ArrayInput; use Symfony\Component\Console\Output\BufferedOutput; -use Zenstruck\Foundry\Tests\Fixture\EntityInAnotherSchema\Article; 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\ResetDatabase\MongoResetterDecorator; -use Zenstruck\Foundry\Tests\Fixture\ResetDatabase\OrmResetterDecorator; 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; From 54c74247c379be1a85592110e60a2285b82cf947 Mon Sep 17 00:00:00 2001 From: Nicolas PHILIPPE Date: Fri, 24 Jan 2025 23:56:48 +0100 Subject: [PATCH 086/102] feat: deprecate when Factories trait is not used in a KernelTestCase (#766) --- .gitignore | 2 +- .php-cs-fixer.dist.php | 7 ++ phpunit.xml.dist | 1 + src/Configuration.php | 3 + src/Exception/FactoriesTraitNotUsed.php | 67 ++++++++++++++ src/PHPUnit/BuildStoryOnTestPrepared.php | 3 + ...TestCaseWithBothTraitsInWrongOrderTest.php | 45 +++++++++ .../KernelTestCaseWithBothTraitsTest.php | 45 +++++++++ .../KernelTestCaseWithFactoriesTraitTest.php | 44 +++++++++ ...TestCaseWithOnlyResetDatabaseTraitTest.php | 23 +++++ ...ernelTestCaseWithoutFactoriesTraitTest.php | 21 +++++ ...lTestCaseWithoutFactoriesTraitTestCase.php | 91 +++++++++++++++++++ .../UnitTestCaseWithFactoriesTraitTest.php | 34 +++++++ .../UnitTestCaseWithoutFactoriesTraitTest.php | 32 +++++++ 14 files changed, 417 insertions(+), 1 deletion(-) create mode 100644 src/Exception/FactoriesTraitNotUsed.php create mode 100644 tests/Integration/ForceFactoriesTraitUsage/KernelTestCaseWithBothTraitsInWrongOrderTest.php create mode 100644 tests/Integration/ForceFactoriesTraitUsage/KernelTestCaseWithBothTraitsTest.php create mode 100644 tests/Integration/ForceFactoriesTraitUsage/KernelTestCaseWithFactoriesTraitTest.php create mode 100644 tests/Integration/ForceFactoriesTraitUsage/KernelTestCaseWithOnlyResetDatabaseTraitTest.php create mode 100644 tests/Integration/ForceFactoriesTraitUsage/KernelTestCaseWithoutFactoriesTraitTest.php create mode 100644 tests/Integration/ForceFactoriesTraitUsage/KernelTestCaseWithoutFactoriesTraitTestCase.php create mode 100644 tests/Integration/ForceFactoriesTraitUsage/UnitTestCaseWithFactoriesTraitTest.php create mode 100644 tests/Integration/ForceFactoriesTraitUsage/UnitTestCaseWithoutFactoriesTraitTest.php diff --git a/.gitignore b/.gitignore index 5ce100385..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 diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php index 5fca00a68..325e8db30 100644 --- a/.php-cs-fixer.dist.php +++ b/.php-cs-fixer.dist.php @@ -7,6 +7,13 @@ \file_get_contents('https://raw.githubusercontent.com/zenstruck/.github/main/.php-cs-fixer.dist.php') ); +$finder = PhpCsFixer\Finder::create() + ->in([__DIR__.'/src', __DIR__.'/tests']) + + // we want to keep the traits in "wrong order" + ->notName('KernelTestCaseWithBothTraitsInWrongOrderTest.php') +; + try { return require $file; } finally { diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 95f1c3e92..15de54d9a 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -20,6 +20,7 @@ tests tests/Integration/ResetDatabase + tests/Integration/ForceFactoriesTraitUsage tests/Integration/ResetDatabase diff --git a/src/Configuration.php b/src/Configuration.php index 048604b4c..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; @@ -90,6 +91,8 @@ public static function instance(): self throw new FoundryNotBooted(); } + FactoriesTraitNotUsed::throwIfComingFromKernelTestCaseWithoutFactoriesTrait(); + return \is_callable(self::$instance) ? (self::$instance)() : self::$instance; } diff --git a/src/Exception/FactoriesTraitNotUsed.php b/src/Exception/FactoriesTraitNotUsed.php new file mode 100644 index 000000000..9d5015c58 --- /dev/null +++ b/src/Exception/FactoriesTraitNotUsed.php @@ -0,0 +1,67 @@ + + * + * 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) + && !(new \ReflectionClass($trace['class']))->hasMethod('_bootFoundry') + ) { + 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/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/tests/Integration/ForceFactoriesTraitUsage/KernelTestCaseWithBothTraitsInWrongOrderTest.php b/tests/Integration/ForceFactoriesTraitUsage/KernelTestCaseWithBothTraitsInWrongOrderTest.php new file mode 100644 index 000000000..f36dec860 --- /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 ResetDatabase, 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/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(); + } +} From bd882b178f0f8349e819f05f98b6bdd023da40a3 Mon Sep 17 00:00:00 2001 From: nikophil <10139766+nikophil@users.noreply.github.com> Date: Fri, 24 Jan 2025 22:57:09 +0000 Subject: [PATCH 087/102] bot: sync with template [skip ci] --- .php-cs-fixer.dist.php | 7 ------- 1 file changed, 7 deletions(-) diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php index 325e8db30..5fca00a68 100644 --- a/.php-cs-fixer.dist.php +++ b/.php-cs-fixer.dist.php @@ -7,13 +7,6 @@ \file_get_contents('https://raw.githubusercontent.com/zenstruck/.github/main/.php-cs-fixer.dist.php') ); -$finder = PhpCsFixer\Finder::create() - ->in([__DIR__.'/src', __DIR__.'/tests']) - - // we want to keep the traits in "wrong order" - ->notName('KernelTestCaseWithBothTraitsInWrongOrderTest.php') -; - try { return require $file; } finally { From ccaca15526bf52867f2e683219d26b180f1e4971 Mon Sep 17 00:00:00 2001 From: nikophil <10139766+nikophil@users.noreply.github.com> Date: Fri, 24 Jan 2025 23:00:16 +0000 Subject: [PATCH 088/102] bot: fix cs [skip ci] --- .../KernelTestCaseWithBothTraitsInWrongOrderTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Integration/ForceFactoriesTraitUsage/KernelTestCaseWithBothTraitsInWrongOrderTest.php b/tests/Integration/ForceFactoriesTraitUsage/KernelTestCaseWithBothTraitsInWrongOrderTest.php index f36dec860..23e72634a 100644 --- a/tests/Integration/ForceFactoriesTraitUsage/KernelTestCaseWithBothTraitsInWrongOrderTest.php +++ b/tests/Integration/ForceFactoriesTraitUsage/KernelTestCaseWithBothTraitsInWrongOrderTest.php @@ -23,7 +23,7 @@ #[RequiresPhpunit('>=11.0')] final class KernelTestCaseWithBothTraitsInWrongOrderTest extends KernelTestCase { - use ResetDatabase, Factories; + use Factories, ResetDatabase; #[Test] public function should_not_throw(): void From 86c5aabcaef2ec6ccfc8aac2597164a0d3f76fa5 Mon Sep 17 00:00:00 2001 From: Nicolas PHILIPPE Date: Mon, 27 Jan 2025 16:30:20 +0100 Subject: [PATCH 089/102] test: assert updates are implicitly persisted (#781) --- src/ORM/AbstractORMPersistenceStrategy.php | 7 +++++-- src/Persistence/PersistenceManager.php | 6 ++---- src/Persistence/PersistentObjectFactory.php | 2 +- .../EntityFactoryRelationshipTestCase.php | 17 +++++++++++++++++ 4 files changed, 25 insertions(+), 7 deletions(-) diff --git a/src/ORM/AbstractORMPersistenceStrategy.php b/src/ORM/AbstractORMPersistenceStrategy.php index fbbb6c2f5..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 diff --git a/src/Persistence/PersistenceManager.php b/src/Persistence/PersistenceManager.php index 79730a6e5..aaf4d621f 100644 --- a/src/Persistence/PersistenceManager.php +++ b/src/Persistence/PersistenceManager.php @@ -155,10 +155,8 @@ public function refresh(object &$object, bool $force = false): object $strategy = $this->strategyFor($object::class); - if (!$force) { - if ($strategy->hasChanges($object)) { - throw RefreshObjectFailed::objectHasUnsavedChanges($object::class); - } + if ($strategy->hasChanges($object)) { + throw RefreshObjectFailed::objectHasUnsavedChanges($object::class); } $om = $strategy->objectManagerFor($object::class); diff --git a/src/Persistence/PersistentObjectFactory.php b/src/Persistence/PersistentObjectFactory.php index 383e8f28b..7267096c9 100644 --- a/src/Persistence/PersistentObjectFactory.php +++ b/src/Persistence/PersistentObjectFactory.php @@ -389,7 +389,7 @@ protected function normalizeObject(object $object): object } try { - return $persistenceManager->refresh($object, force: true); + return $configuration->persistence()->refresh($object); } catch (RefreshObjectFailed|VarExportLogicException) { return $object; } diff --git a/tests/Integration/ORM/EntityRelationship/EntityFactoryRelationshipTestCase.php b/tests/Integration/ORM/EntityRelationship/EntityFactoryRelationshipTestCase.php index c949adc74..8ec50a06c 100644 --- a/tests/Integration/ORM/EntityRelationship/EntityFactoryRelationshipTestCase.php +++ b/tests/Integration/ORM/EntityRelationship/EntityFactoryRelationshipTestCase.php @@ -31,6 +31,7 @@ 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; /** @@ -361,6 +362,22 @@ public function ensure_one_to_many_relations_are_not_pre_persisted(): void } } + /** + * @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')] From 46464cc0b50064fda456dbb813f7e43d65cfba34 Mon Sep 17 00:00:00 2001 From: Nicolas PHILIPPE Date: Sat, 1 Feb 2025 10:28:29 +0100 Subject: [PATCH 090/102] chore(ci): misc improvments in CI permutations (#797) --- .github/workflows/ci.yml | 66 +++++++++++-------- composer.json | 5 +- src/Exception/FactoriesTraitNotUsed.php | 1 - src/ORM/ResetDatabase/BaseOrmResetter.php | 3 +- .../InverseSide.php | 2 +- .../migration-configuration-transactional.php | 2 +- .../migration-configuration.php | 2 +- tests/bootstrap-reset-database.php | 4 +- tests/bootstrap.php | 2 +- 9 files changed, 50 insertions(+), 37 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c0677bd28..f8d084949 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,40 +8,54 @@ on: jobs: tests: - name: P:${{ matrix.php }}, S:${{ matrix.symfony }}, D:${{ matrix.database }}, PU:${{ matrix.phpunit }}${{ 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 ] - phpunit: [ 9, 11 ] + database: [ mysql|mongo ] + phpunit: [ 11 ] # default values: # deps: [ highest ] - # without-dama: [ 0 ] # use-phpunit-extension: [ 0 ] exclude: - {php: 8.1, symfony: 7.1.*} - {php: 8.1, symfony: 7.2.*} - - {php: 8.1, phpunit: 11 } include: + # 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: 11, database: mysql|mongo} - - {php: 8.3, symfony: '*', phpunit: 11, database: pgsql|mongo} - - {php: 8.3, symfony: '*', phpunit: 11, database: pgsql, without-dama: 1} - - {php: 8.3, symfony: '*', phpunit: 11, database: sqlite, without-dama: 1} - - {php: 8.3, symfony: '*', phpunit: 9, database: sqlite, without-dama: 1, deps: lowest} + - {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} - - {php: 8.3, symfony: '*', phpunit: 10, database: mysql|mongo} + + # using Foundry's PHPUnit extension - {php: 8.3, symfony: '*', phpunit: 11, database: mysql|mongo, use-phpunit-extension: 1} - - {php: 8.3, symfony: '*', phpunit: 11, database: mysql|mongo, use-phpunit-extension: 1, without-dama: 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 }} services: @@ -90,7 +104,7 @@ jobs: shell: bash test-reset-database: - name: Test reset database - 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)' || '' }} + 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 @@ -98,17 +112,12 @@ jobs: database: [ mysql, pgsql, sqlite, mysql|mongo ] use-dama: [ 0, 1 ] reset-database-mode: [ schema, migrate ] - migration-configuration-file: ['no', 'migration-configuration', 'migration-configuration-transactional'] + migration-configuration-file: ['no'] + deps: [ highest, lowest ] include: - { database: mongo, migration-configuration-file: 'no', use-dama: 0, reset-database-mode: schema } - exclude: - # there is currently a bug with MySQL and transactional migrations - - database: mysql - migration-configuration-file: 'migration-configuration-transactional' - - reset-database-mode: schema - migration-configuration-file: 'migration-configuration' - - reset-database-mode: schema - migration-configuration-file: 'migration-configuration-transactional' + - { database: pgsql, migration-configuration-file: 'migration-configuration', use-dama: 0, reset-database-mode: migration } + - { database: pgsql, migration-configuration-file: 'migration-configuration-transactional', use-dama: 0, reset-database-mode: migration } env: DATABASE_URL: ${{ contains(matrix.database, 'mysql') && 'mysql://root:root@localhost:3306/foundry?serverVersion=5.7.42' || contains(matrix.database, 'pgsql') && 'postgresql://root:root@localhost:5432/foundry?serverVersion=15' || 'sqlite:///%kernel.project_dir%/var/data.db' }} MONGO_URL: ${{ contains(matrix.database, 'mongo') && 'mongodb://127.0.0.1:27017/dbName?compressors=disabled&gssapiServiceName=mongodb' || '' }} @@ -148,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.* @@ -158,7 +167,12 @@ jobs: run: sudo /etc/init.d/mysql start - name: Test - run: ./phpunit --testsuite reset-database --bootstrap tests/bootstrap-reset-database.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: diff --git a/composer.json b/composer.json index 0b55de8b3..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": "^2|^3", + "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", diff --git a/src/Exception/FactoriesTraitNotUsed.php b/src/Exception/FactoriesTraitNotUsed.php index 9d5015c58..10aeac530 100644 --- a/src/Exception/FactoriesTraitNotUsed.php +++ b/src/Exception/FactoriesTraitNotUsed.php @@ -41,7 +41,6 @@ public static function throwIfComingFromKernelTestCaseWithoutFactoriesTrait(): v && isset($trace['class']) && KernelTestCase::class !== $trace['class'] && \is_a($trace['class'], KernelTestCase::class, allow_string: true) - && !(new \ReflectionClass($trace['class']))->hasMethod('_bootFoundry') ) { self::throwIfClassDoesNotHaveFactoriesTrait($trace['class']); } diff --git a/src/ORM/ResetDatabase/BaseOrmResetter.php b/src/ORM/ResetDatabase/BaseOrmResetter.php index 43e182d38..aa15528a1 100644 --- a/src/ORM/ResetDatabase/BaseOrmResetter.php +++ b/src/ORM/ResetDatabase/BaseOrmResetter.php @@ -20,7 +20,6 @@ use Symfony\Bundle\FrameworkBundle\Console\Application; use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\HttpKernel\KernelInterface; -use Zenstruck\Foundry\ORM\DoctrineOrmVersionGuesser; use function Zenstruck\Foundry\runCommand; @@ -68,7 +67,7 @@ final protected function dropAndResetDatabase(Application $application): void // let's only drop the .db file $dbPath = $connection->getParams()['path'] ?? null; - if (DoctrineOrmVersionGuesser::isOrmV3() && $dbPath && (new Filesystem())->exists($dbPath)) { + if ($dbPath && (new Filesystem())->exists($dbPath)) { \file_put_contents($dbPath, ''); } diff --git a/tests/Fixture/Entity/EdgeCases/InversedOneToOneWithNonNullableOwning/InverseSide.php b/tests/Fixture/Entity/EdgeCases/InversedOneToOneWithNonNullableOwning/InverseSide.php index 450b0899d..f93863fba 100644 --- a/tests/Fixture/Entity/EdgeCases/InversedOneToOneWithNonNullableOwning/InverseSide.php +++ b/tests/Fixture/Entity/EdgeCases/InversedOneToOneWithNonNullableOwning/InverseSide.php @@ -20,7 +20,7 @@ * @author Nicolas PHILIPPE */ #[ORM\Entity] -#[ORM\Table('inversed_one_to_one_with_non_nullable_owning_inverse_side')] +#[ORM\Table('inversed_one_to_one_non_nullable_owning_inverse_side')] class InverseSide extends Base { public function __construct( diff --git a/tests/Fixture/ResetDatabase/migration-configs/migration-configuration-transactional.php b/tests/Fixture/ResetDatabase/migration-configs/migration-configuration-transactional.php index c8f5232f7..fd4821fc6 100644 --- a/tests/Fixture/ResetDatabase/migration-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\\ResetDatabase\\Migrations' => \dirname(__DIR__).'/Migrations', + 'Zenstruck\\Foundry\\Tests\\Fixture\\ResetDatabase\\Migrations' => \dirname(__DIR__, 4).'/var/cache/Migrations', ], 'transactional' => true, ]; diff --git a/tests/Fixture/ResetDatabase/migration-configs/migration-configuration.php b/tests/Fixture/ResetDatabase/migration-configs/migration-configuration.php index b897fc47c..c47f48ae0 100644 --- a/tests/Fixture/ResetDatabase/migration-configs/migration-configuration.php +++ b/tests/Fixture/ResetDatabase/migration-configs/migration-configuration.php @@ -11,7 +11,7 @@ return [ 'migrations_paths' => [ - 'Zenstruck\\Foundry\\Tests\\Fixture\\ResetDatabase\\Migrations' => \dirname(__DIR__, 4).'/var/Migrations', + 'Zenstruck\\Foundry\\Tests\\Fixture\\ResetDatabase\\Migrations' => \dirname(__DIR__, 4).'/var/cache/Migrations', ], 'transactional' => false, ]; diff --git a/tests/bootstrap-reset-database.php b/tests/bootstrap-reset-database.php index 23edea0ca..3c800926b 100644 --- a/tests/bootstrap-reset-database.php +++ b/tests/bootstrap-reset-database.php @@ -21,12 +21,12 @@ $fs = new Filesystem(); -$fs->remove(__DIR__.'/../var'); +$fs->remove(__DIR__.'/../var/cache'); (new Dotenv())->usePutenv()->loadEnv(__DIR__.'/../.env'); if (FoundryTestKernel::usesMigrations()) { - $fs->mkdir(__DIR__.'/../var/Migrations'); + $fs->mkdir(__DIR__.'/../var/cache/Migrations'); $kernel = new ResetDatabaseTestKernel('test', true); $kernel->boot(); diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 363b11221..c8aa9d218 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -16,6 +16,6 @@ $fs = new Filesystem(); -$fs->remove(__DIR__.'/../var'); +$fs->remove(__DIR__.'/../var/cache'); (new Dotenv())->usePutenv()->loadEnv(__DIR__.'/../.env'); From 636ceddb4f720e2f753c46a07d8db5b75480def5 Mon Sep 17 00:00:00 2001 From: Nicolas PHILIPPE Date: Sat, 1 Feb 2025 14:06:48 +0100 Subject: [PATCH 091/102] changelog: update [skip ci] --- CHANGELOG.md | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) 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) From 9032c38178995056300e3fc9a8a1cb125ecb4d08 Mon Sep 17 00:00:00 2001 From: Kai Dederichs Date: Sat, 1 Feb 2025 14:09:33 +0100 Subject: [PATCH 092/102] feat: skip readonly properties on entities when generating factories (#798) Co-authored-by: Nicolas PHILIPPE --- .env | 1 + composer.json | 1 + src/Maker/Factory/FactoryGenerator.php | 2 + src/Maker/Factory/MakeFactoryData.php | 25 ++++++++ src/ZenstruckFoundryBundle.php | 3 + ...tion_of_non_settable_with_always_force.php | 59 +++++++++++++++++++ .../does_not_initialize_non_settable.php | 58 ++++++++++++++++++ tests/Fixture/ObjectWithNonWriteable.php | 41 +++++++++++++ tests/Fixture/TestKernel.php | 4 ++ tests/Fixture/config/always_force.yaml | 3 + tests/Integration/Maker/MakeFactoryTest.php | 31 +++++++++- 11 files changed, 226 insertions(+), 2 deletions(-) create mode 100644 tests/Fixture/Maker/expected/does_force_initialization_of_non_settable_with_always_force.php create mode 100644 tests/Fixture/Maker/expected/does_not_initialize_non_settable.php create mode 100644 tests/Fixture/ObjectWithNonWriteable.php create mode 100644 tests/Fixture/config/always_force.yaml diff --git a/.env b/.env index 673ee0958..f16842ef8 100644 --- a/.env +++ b/.env @@ -10,3 +10,4 @@ MONGO_URL="mongodb://127.0.0.1:27018/dbName?compressors=disabled&gssapiServi USE_DAMA_DOCTRINE_TEST_BUNDLE="0" USE_FOUNDRY_PHPUNIT_EXTENSION="0" PHPUNIT_VERSION="9" # allowed values: 9, 10, 11 +APP_ENV=test diff --git a/composer.json b/composer.json index 1be90a4c6..6bb65bc7c 100644 --- a/composer.json +++ b/composer.json @@ -22,6 +22,7 @@ "symfony/deprecation-contracts": "^2.2|^3.0", "symfony/framework-bundle": "^6.4|^7.0", "symfony/property-access": "^6.4|^7.0", + "symfony/property-info": "^6.4|^7.0", "symfony/var-exporter": "^6.4.9|~7.0.9|^7.1.2", "zenstruck/assert": "^1.4" }, diff --git a/src/Maker/Factory/FactoryGenerator.php b/src/Maker/Factory/FactoryGenerator.php index 76148a4b5..87501c4c8 100644 --- a/src/Maker/Factory/FactoryGenerator.php +++ b/src/Maker/Factory/FactoryGenerator.php @@ -39,6 +39,7 @@ public function __construct( private \Traversable $defaultPropertiesGuessers, private FactoryClassMap $factoryClassMap, private NamespaceGuesser $namespaceGuesser, + private bool $forceProperties = false ) { } @@ -150,6 +151,7 @@ private function createMakeFactoryData(Generator $generator, string $class, Make $this->staticAnalysisTool(), $persisted ?? false, $makeFactoryQuery->addPhpDoc(), + $this->forceProperties ); } diff --git a/src/Maker/Factory/MakeFactoryData.php b/src/Maker/Factory/MakeFactoryData.php index fc9311349..eeb038989 100644 --- a/src/Maker/Factory/MakeFactoryData.php +++ b/src/Maker/Factory/MakeFactoryData.php @@ -15,6 +15,7 @@ use Doctrine\ORM\EntityRepository; use Symfony\Bundle\MakerBundle\Str; use Symfony\Bundle\MakerBundle\Util\ClassNameDetails; +use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor; use Zenstruck\Foundry\ObjectFactory; use Zenstruck\Foundry\Persistence\PersistentProxyObjectFactory; use Zenstruck\Foundry\Persistence\Proxy; @@ -29,6 +30,8 @@ final class MakeFactoryData public const STATIC_ANALYSIS_TOOL_PHPSTAN = 'phpstan'; public const STATIC_ANALYSIS_TOOL_PSALM = 'psalm'; + private static ReflectionExtractor|null $propertyInfo = null; + /** @var list */ private array $uses; /** @var array */ @@ -43,6 +46,7 @@ public function __construct( private string $staticAnalysisTool, private bool $persisted, bool $withPhpDoc, + private bool $forceProperties ) { $this->uses = [ $this->getFactoryClass(), @@ -154,6 +158,22 @@ public function addDefaultProperty(string $propertyName, string $defaultValue): public function getDefaultProperties(): array { $defaultProperties = $this->defaultProperties; + $class = $this->object->getName(); + + /** + * If forceProperties is not set we filter out properties that can not be set because they're either readonly or have no setter. + * Useful for properties that auto generate when the entity is created and can not be changed like a createdAt property for example. + * + * We do this here because we need to get the class of the Entity which only seems to be accessible here. + */ + $defaultProperties = array_filter($defaultProperties, function (string $propertyName) use ($class): bool { + if (true === $this->forceProperties) { + return true; + } + + return self::propertyInfo()->isWritable($class, $propertyName) || self::propertyInfo()->isInitializable($class, $propertyName); + }, ARRAY_FILTER_USE_KEY); + \ksort($defaultProperties); return $defaultProperties; @@ -189,4 +209,9 @@ public function addEnumDefaultProperty(string $propertyName, string $enumClass): "self::faker()->randomElement({$enumShortClassName}::cases()),", ); } + + private static function propertyInfo(): ReflectionExtractor + { + return self::$propertyInfo ??= new ReflectionExtractor(); + } } diff --git a/src/ZenstruckFoundryBundle.php b/src/ZenstruckFoundryBundle.php index e8e188be6..52a55e91f 100644 --- a/src/ZenstruckFoundryBundle.php +++ b/src/ZenstruckFoundryBundle.php @@ -231,6 +231,9 @@ public function loadExtension(array $config, ContainerConfigurator $configurator if (!isset($bundles['DoctrineBundle']) && !isset($bundles['DoctrineMongoDBBundle'])) { $container->removeDefinition('.zenstruck_foundry.maker.factory.doctrine_scalar_fields_default_properties_guesser'); } + + $container->getDefinition('.zenstruck_foundry.maker.factory.generator') + ->setArgument('$forceProperties', $config['instantiator']['always_force_properties'] ?? false); } else { $configurator->import('../config/command_stubs.php'); } diff --git a/tests/Fixture/Maker/expected/does_force_initialization_of_non_settable_with_always_force.php b/tests/Fixture/Maker/expected/does_force_initialization_of_non_settable_with_always_force.php new file mode 100644 index 000000000..322d270c5 --- /dev/null +++ b/tests/Fixture/Maker/expected/does_force_initialization_of_non_settable_with_always_force.php @@ -0,0 +1,59 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace App\Factory; + +use Zenstruck\Foundry\ObjectFactory; +use Zenstruck\Foundry\Tests\Fixture\ObjectWithNonWriteable; + +/** + * @extends ObjectFactory + */ +final class ObjectWithNonWriteableFactory extends ObjectFactory +{ + /** + * @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#factories-as-services + * + * @todo inject services if required + */ + public function __construct() + { + } + + public static function class(): string + { + return ObjectWithNonWriteable::class; + } + + /** + * @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#model-factories + * + * @todo add your default values here + */ + protected function defaults(): array|callable + { + return [ + 'bar' => self::faker()->sentence(), + 'baz' => self::faker()->sentence(), + 'foo' => self::faker()->sentence(), + ]; + } + + /** + * @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#initialization + */ + protected function initialize(): static + { + return $this + // ->afterInstantiate(function(ObjectWithNonWriteable $objectWithNonWriteable): void {}) + ; + } +} diff --git a/tests/Fixture/Maker/expected/does_not_initialize_non_settable.php b/tests/Fixture/Maker/expected/does_not_initialize_non_settable.php new file mode 100644 index 000000000..8d7b7948a --- /dev/null +++ b/tests/Fixture/Maker/expected/does_not_initialize_non_settable.php @@ -0,0 +1,58 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace App\Factory; + +use Zenstruck\Foundry\ObjectFactory; +use Zenstruck\Foundry\Tests\Fixture\ObjectWithNonWriteable; + +/** + * @extends ObjectFactory + */ +final class ObjectWithNonWriteableFactory extends ObjectFactory +{ + /** + * @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#factories-as-services + * + * @todo inject services if required + */ + public function __construct() + { + } + + public static function class(): string + { + return ObjectWithNonWriteable::class; + } + + /** + * @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#model-factories + * + * @todo add your default values here + */ + protected function defaults(): array|callable + { + return [ + 'baz' => self::faker()->sentence(), + 'foo' => self::faker()->sentence(), + ]; + } + + /** + * @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#initialization + */ + protected function initialize(): static + { + return $this + // ->afterInstantiate(function(ObjectWithNonWriteable $objectWithNonWriteable): void {}) + ; + } +} diff --git a/tests/Fixture/ObjectWithNonWriteable.php b/tests/Fixture/ObjectWithNonWriteable.php new file mode 100644 index 000000000..486453bf1 --- /dev/null +++ b/tests/Fixture/ObjectWithNonWriteable.php @@ -0,0 +1,41 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Tests\Fixture; + +final class ObjectWithNonWriteable +{ + private string $bar; + // @phpstan-ignore-next-line We do not want assign a default value to this so the factory sets one + private string $baz; + + public function __construct( + public readonly string $foo + ) { + $this->bar = 'bar'; + } + + public function getBaz(): string + { + return $this->baz; + } + + public function setBaz(string $baz): ObjectWithNonWriteable + { + $this->baz = $baz; + return $this; + } + + public function getBar(): string + { + return $this->bar; + } +} diff --git a/tests/Fixture/TestKernel.php b/tests/Fixture/TestKernel.php index edb4ddfa5..341105cd0 100644 --- a/tests/Fixture/TestKernel.php +++ b/tests/Fixture/TestKernel.php @@ -35,6 +35,10 @@ protected function configureContainer(ContainerBuilder $c, LoaderInterface $load { parent::configureContainer($c, $loader); + if ($this->getEnvironment() !== 'test') { + $loader->load(\sprintf('%s/config/%s.yaml', __DIR__, $this->getEnvironment())); + } + $c->loadFromExtension('zenstruck_foundry', [ 'orm' => [ 'reset' => [ diff --git a/tests/Fixture/config/always_force.yaml b/tests/Fixture/config/always_force.yaml new file mode 100644 index 000000000..ab286dba7 --- /dev/null +++ b/tests/Fixture/config/always_force.yaml @@ -0,0 +1,3 @@ +zenstruck_foundry: + instantiator: + always_force_properties: true # always "force set" properties diff --git a/tests/Integration/Maker/MakeFactoryTest.php b/tests/Integration/Maker/MakeFactoryTest.php index 5d8af22d8..8bd3411a0 100644 --- a/tests/Integration/Maker/MakeFactoryTest.php +++ b/tests/Integration/Maker/MakeFactoryTest.php @@ -23,6 +23,7 @@ use Zenstruck\Foundry\Tests\Fixture\Entity\WithEmbeddableEntity; use Zenstruck\Foundry\Tests\Fixture\Object1; use Zenstruck\Foundry\Tests\Fixture\ObjectWithEnum; +use Zenstruck\Foundry\Tests\Fixture\ObjectWithNonWriteable; /** * @author Kevin Bond @@ -421,14 +422,40 @@ public function can_create_factory_with_default_enum(): void $this->assertFileFromMakerSameAsExpectedFile(self::tempFile('src/Factory/ObjectWithEnumFactory.php')); } + /** + * @test + */ + public function does_not_initialize_non_settable(): void + { + $tester = $this->makeFactoryCommandTester(); + + $tester->execute(['class' => ObjectWithNonWriteable::class, '--no-persistence' => true]); + + $this->assertFileFromMakerSameAsExpectedFile(self::tempFile('src/Factory/ObjectWithNonWriteableFactory.php')); + } + + /** + * @test + */ + public function does_force_initialization_of_non_settable_with_always_force(): void + { + $tester = $this->makeFactoryCommandTester('always_force'); + + $tester->execute(['class' => ObjectWithNonWriteable::class, '--no-persistence' => true]); + + $this->assertFileFromMakerSameAsExpectedFile(self::tempFile('src/Factory/ObjectWithNonWriteableFactory.php')); + } + private function emulateSCAToolEnabled(string $scaToolFilePath): void { \mkdir(\dirname($scaToolFilePath), 0777, true); \touch($scaToolFilePath); } - private function makeFactoryCommandTester(): CommandTester + private function makeFactoryCommandTester(string $appEnv = 'test'): CommandTester { - return new CommandTester((new Application(self::bootKernel()))->find('make:factory')); + return new CommandTester((new Application(self::bootKernel([ + 'environment' => $appEnv, + ])))->find('make:factory')); } } From 5681bb9bf7eb6d76dbaa82b73e1a76b6997e43a6 Mon Sep 17 00:00:00 2001 From: nikophil <10139766+nikophil@users.noreply.github.com> Date: Sat, 1 Feb 2025 13:12:45 +0000 Subject: [PATCH 093/102] bot: fix cs [skip ci] --- src/Maker/Factory/FactoryGenerator.php | 2 +- src/Maker/Factory/MakeFactoryData.php | 8 ++++---- tests/Fixture/ObjectWithNonWriteable.php | 5 +++-- tests/Fixture/TestKernel.php | 2 +- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/Maker/Factory/FactoryGenerator.php b/src/Maker/Factory/FactoryGenerator.php index 87501c4c8..f5cfa02cb 100644 --- a/src/Maker/Factory/FactoryGenerator.php +++ b/src/Maker/Factory/FactoryGenerator.php @@ -39,7 +39,7 @@ public function __construct( private \Traversable $defaultPropertiesGuessers, private FactoryClassMap $factoryClassMap, private NamespaceGuesser $namespaceGuesser, - private bool $forceProperties = false + private bool $forceProperties = false, ) { } diff --git a/src/Maker/Factory/MakeFactoryData.php b/src/Maker/Factory/MakeFactoryData.php index eeb038989..82dce155d 100644 --- a/src/Maker/Factory/MakeFactoryData.php +++ b/src/Maker/Factory/MakeFactoryData.php @@ -30,7 +30,7 @@ final class MakeFactoryData public const STATIC_ANALYSIS_TOOL_PHPSTAN = 'phpstan'; public const STATIC_ANALYSIS_TOOL_PSALM = 'psalm'; - private static ReflectionExtractor|null $propertyInfo = null; + private static ?ReflectionExtractor $propertyInfo = null; /** @var list */ private array $uses; @@ -46,7 +46,7 @@ public function __construct( private string $staticAnalysisTool, private bool $persisted, bool $withPhpDoc, - private bool $forceProperties + private bool $forceProperties, ) { $this->uses = [ $this->getFactoryClass(), @@ -166,13 +166,13 @@ public function getDefaultProperties(): array * * We do this here because we need to get the class of the Entity which only seems to be accessible here. */ - $defaultProperties = array_filter($defaultProperties, function (string $propertyName) use ($class): bool { + $defaultProperties = \array_filter($defaultProperties, function(string $propertyName) use ($class): bool { if (true === $this->forceProperties) { return true; } return self::propertyInfo()->isWritable($class, $propertyName) || self::propertyInfo()->isInitializable($class, $propertyName); - }, ARRAY_FILTER_USE_KEY); + }, \ARRAY_FILTER_USE_KEY); \ksort($defaultProperties); diff --git a/tests/Fixture/ObjectWithNonWriteable.php b/tests/Fixture/ObjectWithNonWriteable.php index 486453bf1..3a0ef8a90 100644 --- a/tests/Fixture/ObjectWithNonWriteable.php +++ b/tests/Fixture/ObjectWithNonWriteable.php @@ -18,7 +18,7 @@ final class ObjectWithNonWriteable private string $baz; public function __construct( - public readonly string $foo + public readonly string $foo, ) { $this->bar = 'bar'; } @@ -28,9 +28,10 @@ public function getBaz(): string return $this->baz; } - public function setBaz(string $baz): ObjectWithNonWriteable + public function setBaz(string $baz): self { $this->baz = $baz; + return $this; } diff --git a/tests/Fixture/TestKernel.php b/tests/Fixture/TestKernel.php index 341105cd0..998cd70c7 100644 --- a/tests/Fixture/TestKernel.php +++ b/tests/Fixture/TestKernel.php @@ -35,7 +35,7 @@ protected function configureContainer(ContainerBuilder $c, LoaderInterface $load { parent::configureContainer($c, $loader); - if ($this->getEnvironment() !== 'test') { + if ('test' !== $this->getEnvironment()) { $loader->load(\sprintf('%s/config/%s.yaml', __DIR__, $this->getEnvironment())); } From 34101a7590e5fd3a2352bbeedd7804a2d895347e Mon Sep 17 00:00:00 2001 From: Nicolas PHILIPPE Date: Sun, 2 Feb 2025 16:07:04 +0100 Subject: [PATCH 094/102] feat: dispatch events (#790) --- composer.json | 1 + config/services.php | 1 + docs/index.rst | 70 +++++++++++++++---- src/Configuration.php | 12 ++++ src/Object/Event/AfterInstantiate.php | 34 +++++++++ src/Object/Event/BeforeInstantiate.php | 35 ++++++++++ src/ObjectFactory.php | 29 ++++++++ src/Persistence/Event/AfterPersist.php | 34 +++++++++ src/Persistence/PersistentObjectFactory.php | 32 ++++++--- .../Entity/EntityForEventListeners.php | 27 +++++++ .../Events/FactoryWithEventListeners.php | 38 ++++++++++ tests/Fixture/Events/FoundryEventListener.php | 53 ++++++++++++++ tests/Fixture/TestKernel.php | 3 + tests/Integration/Persistence/EventsTest.php | 43 ++++++++++++ 14 files changed, 390 insertions(+), 22 deletions(-) create mode 100644 src/Object/Event/AfterInstantiate.php create mode 100644 src/Object/Event/BeforeInstantiate.php create mode 100644 src/Persistence/Event/AfterPersist.php create mode 100644 tests/Fixture/Entity/EntityForEventListeners.php create mode 100644 tests/Fixture/Events/FactoryWithEventListeners.php create mode 100644 tests/Fixture/Events/FoundryEventListener.php create mode 100644 tests/Integration/Persistence/EventsTest.php diff --git a/composer.json b/composer.json index 6bb65bc7c..beeb7cafb 100644 --- a/composer.json +++ b/composer.json @@ -20,6 +20,7 @@ "doctrine/persistence": "^2.0|^3.0", "fakerphp/faker": "^1.23", "symfony/deprecation-contracts": "^2.2|^3.0", + "symfony/event-dispatcher": "^6.4|^7.0", "symfony/framework-bundle": "^6.4|^7.0", "symfony/property-access": "^6.4|^7.0", "symfony/property-info": "^6.4|^7.0", diff --git a/config/services.php b/config/services.php index cd3fd6fd9..94b92aa01 100644 --- a/config/services.php +++ b/config/services.php @@ -32,6 +32,7 @@ service('.zenstruck_foundry.instantiator'), service('.zenstruck_foundry.story_registry'), service('.zenstruck_foundry.persistence_manager')->nullOnInvalid(), + service('event_dispatcher'), ]) ->public() ; diff --git a/docs/index.rst b/docs/index.rst index 0a0aab2f3..7921a68c4 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -552,10 +552,10 @@ random data for your factories: faker: service: my_faker # service id for your own instance of Faker\Generator -Events / Hooks -~~~~~~~~~~~~~~ +Hooks +~~~~~ -The following events can be added to factories. Multiple event callbacks can be added, they are run in the order +The following hooks can be added to factories. Multiple hooks callbacks can be added, they are run in the order they were added. :: @@ -564,28 +564,28 @@ they were added. use Zenstruck\Foundry\Proxy; PostFactory::new() - ->beforeInstantiate(function(array $attributes, string $class, static $factory): array { - // $attributes is what will be used to instantiate the object, manipulate as required + ->beforeInstantiate(function(array $parameters, string $class, static $factory): array { + // $parameters is what will be used to instantiate the object, manipulate as required // $class is the class of the object being instantiated // $factory is the factory instance which creates the object - $attributes['title'] = 'Different title'; + $parameters['title'] = 'Different title'; - return $attributes; // must return the final $attributes + return $parameters; // must return the final $parameters }) - ->afterInstantiate(function(Post $object, array $attributes, static $factory): void { + ->afterInstantiate(function(Post $object, array $parameters, static $factory): void { // $object is the instantiated object - // $attributes contains the attributes used to instantiate the object and any extras + // $parameters contains the attributes used to instantiate the object and any extras // $factory is the factory instance which creates the object }) - ->afterPersist(function(Post $object, array $attributes, static $factory) { + ->afterPersist(function(Post $object, array $parameters, static $factory) { // this event is only called if the object was persisted // $object is the persisted Post object - // $attributes contains the attributes used to instantiate the object and any extras + // $parameters contains the attributes used to instantiate the object and any extras // $factory is the factory instance which creates the object }) // multiple events are allowed - ->beforeInstantiate(function($attributes) { return $attributes; }) + ->beforeInstantiate(function($parameters) { return $parameters; }) ->afterInstantiate(function() {}) ->afterPersist(function() {}) ; @@ -603,6 +603,52 @@ You can also add hooks directly in your factory class: Read `Initialization`_ to learn more about the ``initialize()`` method. +Events +~~~~~~ + +In addition to hooks, Foundry also leverages `symfony/event-dispatcher` and dispatches events that you can listen to, +allowing to create hooks globally, as Symfony services: + +:: + + use Symfony\Component\EventDispatcher\Attribute\AsEventListener; + use Zenstruck\Foundry\Object\Event\AfterInstantiate; + use Zenstruck\Foundry\Object\Event\BeforeInstantiate; + use Zenstruck\Foundry\Persistence\Event\AfterPersist; + + final class FoundryEventListener + { + #[AsEventListener] + public function beforeInstantiate(BeforeInstantiate $event): void + { + // do something before the object is instantiated: + // $event->parameters is what will be used to instantiate the object, manipulate as required + // $event->objectClass is the class of the object being instantiated + // $event->factory is the factory instance which creates the object + } + + #[AsEventListener] + public function afterInstantiate(AfterInstantiate $event): void + { + // $event->object is the instantiated object + // $event->parameters contains the attributes used to instantiate the object and any extras + // $event->factory is the factory instance which creates the object + } + + #[AsEventListener] + public function afterPersist(AfterPersist $event): void + { + // this event is only called if the object was persisted + // $event->object is the persisted Post object + // $event->parameters contains the attributes used to instantiate the object and any extras + // $event->factory is the factory instance which creates the object + } + } + +.. versionadded:: 2.4 + + Those events are triggered since Foundry 2.4. + Initialization ~~~~~~~~~~~~~~ diff --git a/src/Configuration.php b/src/Configuration.php index 8e0dc219d..3bf880287 100644 --- a/src/Configuration.php +++ b/src/Configuration.php @@ -12,6 +12,7 @@ namespace Zenstruck\Foundry; use Faker; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Zenstruck\Foundry\Exception\FactoriesTraitNotUsed; use Zenstruck\Foundry\Exception\FoundryNotBooted; use Zenstruck\Foundry\Exception\PersistenceDisabled; @@ -51,6 +52,7 @@ public function __construct( callable $instantiator, public readonly StoryRegistry $stories, private readonly ?PersistenceManager $persistence = null, + private readonly ?EventDispatcherInterface $eventDispatcher = null, ) { $this->instantiator = $instantiator; } @@ -80,6 +82,16 @@ public function assertPersistenceEnabled(): void } } + public function hasEventDispatcher(): bool + { + return (bool) $this->eventDispatcher; + } + + public function eventDispatcher(): EventDispatcherInterface + { + return $this->eventDispatcher ?? throw new \RuntimeException('No event dispatcher configured.'); + } + public function inADataProvider(): bool { return $this->bootedForDataProvider; diff --git a/src/Object/Event/AfterInstantiate.php b/src/Object/Event/AfterInstantiate.php new file mode 100644 index 000000000..7bbef74f5 --- /dev/null +++ b/src/Object/Event/AfterInstantiate.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\Object\Event; + +use Zenstruck\Foundry\Factory; +use Zenstruck\Foundry\ObjectFactory; + +/** + * @author Nicolas PHILIPPE + * + * @phpstan-import-type Parameters from Factory + */ +final class AfterInstantiate +{ + public function __construct( + public readonly object $object, + /** @phpstan-var Parameters */ + public readonly array $parameters, + /** @var ObjectFactory */ + public readonly ObjectFactory $factory, + ) { + } +} diff --git a/src/Object/Event/BeforeInstantiate.php b/src/Object/Event/BeforeInstantiate.php new file mode 100644 index 000000000..b5ad5cb38 --- /dev/null +++ b/src/Object/Event/BeforeInstantiate.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\Object\Event; + +use Zenstruck\Foundry\Factory; +use Zenstruck\Foundry\ObjectFactory; + +/** + * @author Nicolas PHILIPPE + * + * @phpstan-import-type Parameters from Factory + */ +final class BeforeInstantiate +{ + public function __construct( + /** @phpstan-var Parameters */ + public array $parameters, + /** @var class-string */ + public readonly string $objectClass, + /** @var ObjectFactory */ + public readonly ObjectFactory $factory, + ) { + } +} diff --git a/src/ObjectFactory.php b/src/ObjectFactory.php index 8d69933f4..4abf7c485 100644 --- a/src/ObjectFactory.php +++ b/src/ObjectFactory.php @@ -11,6 +11,8 @@ namespace Zenstruck\Foundry; +use Zenstruck\Foundry\Object\Event\AfterInstantiate; +use Zenstruck\Foundry\Object\Event\BeforeInstantiate; use Zenstruck\Foundry\Object\Instantiator; /** @@ -102,4 +104,31 @@ public function afterInstantiate(callable $callback): static return $clone; } + + /** + * @internal + */ + protected function initializeInternal(): static + { + if (!Configuration::instance()->hasEventDispatcher()) { + return $this; + } + + return $this->beforeInstantiate( + static function(array $parameters, string $objectClass, self $usedFactory): array { + Configuration::instance()->eventDispatcher()->dispatch( + $hook = new BeforeInstantiate($parameters, $objectClass, $usedFactory) + ); + + return $hook->parameters; + } + ) + ->afterInstantiate( + static function(object $object, array $parameters, self $usedFactory): void { + Configuration::instance()->eventDispatcher()->dispatch( + new AfterInstantiate($object, $parameters, $usedFactory) + ); + } + ); + } } diff --git a/src/Persistence/Event/AfterPersist.php b/src/Persistence/Event/AfterPersist.php new file mode 100644 index 000000000..eb89763c2 --- /dev/null +++ b/src/Persistence/Event/AfterPersist.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\Persistence\Event; + +use Zenstruck\Foundry\Factory; +use Zenstruck\Foundry\Persistence\PersistentObjectFactory; + +/** + * @author Nicolas PHILIPPE + * + * @phpstan-import-type Parameters from Factory + */ +final class AfterPersist +{ + public function __construct( + public readonly object $object, + /** @phpstan-var Parameters */ + public readonly array $parameters, + /** @var PersistentObjectFactory */ + public readonly PersistentObjectFactory $factory, + ) { + } +} diff --git a/src/Persistence/PersistentObjectFactory.php b/src/Persistence/PersistentObjectFactory.php index 7267096c9..e426da4d8 100644 --- a/src/Persistence/PersistentObjectFactory.php +++ b/src/Persistence/PersistentObjectFactory.php @@ -19,6 +19,7 @@ use Zenstruck\Foundry\Factory; use Zenstruck\Foundry\FactoryCollection; use Zenstruck\Foundry\ObjectFactory; +use Zenstruck\Foundry\Persistence\Event\AfterPersist; use Zenstruck\Foundry\Persistence\Exception\NotEnoughObjects; use Zenstruck\Foundry\Persistence\Exception\RefreshObjectFailed; @@ -406,19 +407,30 @@ final protected function isPersisting(): bool 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; + // Schedule any new object for insert right after instantiation + $factory = parent::initializeInternal() + ->afterInstantiate( + static function(object $object, array $parameters, PersistentObjectFactory $factoryUsed): void { + if (!$factoryUsed->isPersisting()) { + return; + } + + Configuration::instance()->persistence()->scheduleForInsert($object); } - - Configuration::instance()->persistence()->scheduleForInsert($object); + ); + + if (!Configuration::instance()->hasEventDispatcher()) { + return $factory; + }; + + // Dispatch event after persist + return $factory->afterPersist( + static function(object $object, array $parameters, self $factoryUsed): void { + Configuration::instance()->eventDispatcher()->dispatch( + new AfterPersist($object, $parameters, $factoryUsed) + ); } ); } diff --git a/tests/Fixture/Entity/EntityForEventListeners.php b/tests/Fixture/Entity/EntityForEventListeners.php new file mode 100644 index 000000000..4d36182b5 --- /dev/null +++ b/tests/Fixture/Entity/EntityForEventListeners.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Tests\Fixture\Entity; + +use Doctrine\ORM\Mapping as ORM; +use Zenstruck\Foundry\Tests\Fixture\Model\Base; + +#[ORM\Entity] +class EntityForEventListeners extends Base +{ + public function __construct( + #[ORM\Column()] + public string $name, + ) { + } +} diff --git a/tests/Fixture/Events/FactoryWithEventListeners.php b/tests/Fixture/Events/FactoryWithEventListeners.php new file mode 100644 index 000000000..dcda63c7b --- /dev/null +++ b/tests/Fixture/Events/FactoryWithEventListeners.php @@ -0,0 +1,38 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Tests\Fixture\Events; + +use Zenstruck\Foundry\Persistence\PersistentObjectFactory; +use Zenstruck\Foundry\Tests\Fixture\Entity\EntityForEventListeners; + +/** + * @extends PersistentObjectFactory + */ +final class FactoryWithEventListeners extends PersistentObjectFactory +{ + public static function class(): string + { + return EntityForEventListeners::class; + } + + /** + * @return array + */ + protected function defaults(): array + { + return [ + 'name' => self::faker()->sentence(), + ]; + } +} diff --git a/tests/Fixture/Events/FoundryEventListener.php b/tests/Fixture/Events/FoundryEventListener.php new file mode 100644 index 000000000..ddc00e4f0 --- /dev/null +++ b/tests/Fixture/Events/FoundryEventListener.php @@ -0,0 +1,53 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Tests\Fixture\Events; + +use Symfony\Component\EventDispatcher\Attribute\AsEventListener; +use Zenstruck\Foundry\Object\Event\AfterInstantiate; +use Zenstruck\Foundry\Object\Event\BeforeInstantiate; +use Zenstruck\Foundry\Persistence\Event\AfterPersist; +use Zenstruck\Foundry\Tests\Fixture\Entity\EntityForEventListeners; + +final class FoundryEventListener +{ + #[AsEventListener] + public function beforeInstantiate(BeforeInstantiate $event): void + { + if (EntityForEventListeners::class !== $event->objectClass) { + return; + } + + $event->parameters['name'] = "{$event->parameters['name']}\nBeforeInstantiate"; + } + + #[AsEventListener] + public function afterInstantiate(AfterInstantiate $event): void + { + if (!$event->object instanceof EntityForEventListeners) { + return; + } + + $event->object->name = "{$event->object->name}\nAfterInstantiate"; + } + + #[AsEventListener] + public function afterPersist(AfterPersist $event): void + { + if (!$event->object instanceof EntityForEventListeners) { + return; + } + + $event->object->name = "{$event->object->name}\nAfterPersist"; + } +} diff --git a/tests/Fixture/TestKernel.php b/tests/Fixture/TestKernel.php index 998cd70c7..e25dcbacf 100644 --- a/tests/Fixture/TestKernel.php +++ b/tests/Fixture/TestKernel.php @@ -17,6 +17,7 @@ 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\Events\FoundryEventListener; use Zenstruck\Foundry\Tests\Fixture\Stories\ServiceStory; /** @@ -50,5 +51,7 @@ protected function configureContainer(ContainerBuilder $c, LoaderInterface $load $c->register(ArrayFactory::class)->setAutowired(true)->setAutoconfigured(true); $c->register(Object1Factory::class)->setAutowired(true)->setAutoconfigured(true); $c->register(ServiceStory::class)->setAutowired(true)->setAutoconfigured(true); + + $c->register(FoundryEventListener::class)->setAutowired(true)->setAutoconfigured(true); } } diff --git a/tests/Integration/Persistence/EventsTest.php b/tests/Integration/Persistence/EventsTest.php new file mode 100644 index 000000000..358d0f3bc --- /dev/null +++ b/tests/Integration/Persistence/EventsTest.php @@ -0,0 +1,43 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Tests\Integration\Persistence; + +use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; +use Zenstruck\Foundry\Test\Factories; +use Zenstruck\Foundry\Test\ResetDatabase; +use Zenstruck\Foundry\Tests\Fixture\Events\FactoryWithEventListeners; +use Zenstruck\Foundry\Tests\Integration\RequiresORM; + +final class EventsTest extends KernelTestCase +{ + use Factories, RequiresORM, ResetDatabase; + + /** + * @test + */ + public function it_can_call_hooks(): void + { + $address = FactoryWithEventListeners::createOne(['name' => 'events']); + + self::assertSame( + <<name + ); + } +} From 409a367f5257a71e6f3ed22d6e5b434762383f9e Mon Sep 17 00:00:00 2001 From: nikophil <10139766+nikophil@users.noreply.github.com> Date: Sun, 2 Feb 2025 15:10:20 +0000 Subject: [PATCH 095/102] bot: fix cs [skip ci] --- src/Persistence/PersistentObjectFactory.php | 2 +- tests/Fixture/TestKernel.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Persistence/PersistentObjectFactory.php b/src/Persistence/PersistentObjectFactory.php index e426da4d8..5179fdb2c 100644 --- a/src/Persistence/PersistentObjectFactory.php +++ b/src/Persistence/PersistentObjectFactory.php @@ -423,7 +423,7 @@ static function(object $object, array $parameters, PersistentObjectFactory $fact if (!Configuration::instance()->hasEventDispatcher()) { return $factory; - }; + } // Dispatch event after persist return $factory->afterPersist( diff --git a/tests/Fixture/TestKernel.php b/tests/Fixture/TestKernel.php index e25dcbacf..283501d98 100644 --- a/tests/Fixture/TestKernel.php +++ b/tests/Fixture/TestKernel.php @@ -15,9 +15,9 @@ use Symfony\Component\Config\Loader\LoaderInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; use Zenstruck\Foundry\ORM\ResetDatabase\ResetDatabaseMode; +use Zenstruck\Foundry\Tests\Fixture\Events\FoundryEventListener; use Zenstruck\Foundry\Tests\Fixture\Factories\ArrayFactory; use Zenstruck\Foundry\Tests\Fixture\Factories\Object1Factory; -use Zenstruck\Foundry\Tests\Fixture\Events\FoundryEventListener; use Zenstruck\Foundry\Tests\Fixture\Stories\ServiceStory; /** From 207562fe1be229aaf02a891ae20d6a8b5b49a56a Mon Sep 17 00:00:00 2001 From: Nicolas PHILIPPE Date: Sun, 2 Feb 2025 19:43:08 +0100 Subject: [PATCH 096/102] fix: remove APP_ENV from .env (#803) --- .env | 1 - tests/Fixture/TestKernel.php | 2 +- tests/Integration/Maker/MakeFactoryTest.php | 8 +++----- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/.env b/.env index f16842ef8..673ee0958 100644 --- a/.env +++ b/.env @@ -10,4 +10,3 @@ MONGO_URL="mongodb://127.0.0.1:27018/dbName?compressors=disabled&gssapiServi USE_DAMA_DOCTRINE_TEST_BUNDLE="0" USE_FOUNDRY_PHPUNIT_EXTENSION="0" PHPUNIT_VERSION="9" # allowed values: 9, 10, 11 -APP_ENV=test diff --git a/tests/Fixture/TestKernel.php b/tests/Fixture/TestKernel.php index 283501d98..c9e3be5ec 100644 --- a/tests/Fixture/TestKernel.php +++ b/tests/Fixture/TestKernel.php @@ -36,7 +36,7 @@ protected function configureContainer(ContainerBuilder $c, LoaderInterface $load { parent::configureContainer($c, $loader); - if ('test' !== $this->getEnvironment()) { + if ('dev' !== $this->getEnvironment()) { $loader->load(\sprintf('%s/config/%s.yaml', __DIR__, $this->getEnvironment())); } diff --git a/tests/Integration/Maker/MakeFactoryTest.php b/tests/Integration/Maker/MakeFactoryTest.php index 8bd3411a0..39e4a7a47 100644 --- a/tests/Integration/Maker/MakeFactoryTest.php +++ b/tests/Integration/Maker/MakeFactoryTest.php @@ -439,7 +439,7 @@ public function does_not_initialize_non_settable(): void */ public function does_force_initialization_of_non_settable_with_always_force(): void { - $tester = $this->makeFactoryCommandTester('always_force'); + $tester = $this->makeFactoryCommandTester(['environment' => 'always_force']); $tester->execute(['class' => ObjectWithNonWriteable::class, '--no-persistence' => true]); @@ -452,10 +452,8 @@ private function emulateSCAToolEnabled(string $scaToolFilePath): void \touch($scaToolFilePath); } - private function makeFactoryCommandTester(string $appEnv = 'test'): CommandTester + private function makeFactoryCommandTester(array $options = []): CommandTester { - return new CommandTester((new Application(self::bootKernel([ - 'environment' => $appEnv, - ])))->find('make:factory')); + return new CommandTester((new Application(self::bootKernel($options)))->find('make:factory')); } } From f76cba2f0221d3098f12d012bedde53cb38b3d37 Mon Sep 17 00:00:00 2001 From: Nicolas PHILIPPE Date: Mon, 3 Feb 2025 09:45:40 +0100 Subject: [PATCH 097/102] fix: fix deprecation message for Factories trait (#806) --- src/Exception/FactoriesTraitNotUsed.php | 4 ++-- .../KernelTestCaseWithoutFactoriesTraitTestCase.php | 11 ++++++++--- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/Exception/FactoriesTraitNotUsed.php b/src/Exception/FactoriesTraitNotUsed.php index 10aeac530..810579eb1 100644 --- a/src/Exception/FactoriesTraitNotUsed.php +++ b/src/Exception/FactoriesTraitNotUsed.php @@ -57,8 +57,8 @@ public static function throwIfClassDoesNotHaveFactoriesTrait(string $class): voi 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, + 'In order to use Foundry correctly, you must use the trait "%s" in your "%s" tests. This will throw an exception in 3.0.', + Factories::class, $class ); } diff --git a/tests/Integration/ForceFactoriesTraitUsage/KernelTestCaseWithoutFactoriesTraitTestCase.php b/tests/Integration/ForceFactoriesTraitUsage/KernelTestCaseWithoutFactoriesTraitTestCase.php index ba73e8a73..a332ade2e 100644 --- a/tests/Integration/ForceFactoriesTraitUsage/KernelTestCaseWithoutFactoriesTraitTestCase.php +++ b/tests/Integration/ForceFactoriesTraitUsage/KernelTestCaseWithoutFactoriesTraitTestCase.php @@ -44,7 +44,7 @@ public function not_using_foundry_should_not_throw_even_when_container_is_used() #[IgnoreDeprecations] public function using_foundry_without_trait_should_throw(): void { - $this->expectUserDeprecationMessageMatches('/In order to use Foundry, you must use the trait/'); + $this->assertDeprecation(); Object1Factory::createOne(); } @@ -53,7 +53,7 @@ public function using_foundry_without_trait_should_throw(): void #[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/'); + $this->assertDeprecation(); self::getContainer()->get('.zenstruck_foundry.configuration'); @@ -65,7 +65,7 @@ public function using_foundry_without_trait_should_throw_even_when_kernel_is_boo #[IgnoreDeprecations] public function using_a_story_without_factories_trait_should_throw(): void { - $this->expectUserDeprecationMessageMatches('/In order to use Foundry, you must use the trait/'); + $this->assertDeprecation(); ObjectStory::load(); } @@ -88,4 +88,9 @@ public static function _shutdownFoundry(): void { Configuration::shutdown(); } + + private function assertDeprecation(): void + { + $this->expectUserDeprecationMessageMatches('/In order to use Foundry correctly, you must use the trait "Zenstruck\\\\Foundry\\\\Test\\\\Factories/'); + } } From 1c3f73a5cab4717accf5e0629544864a518d53a8 Mon Sep 17 00:00:00 2001 From: Nicolas PHILIPPE Date: Tue, 4 Feb 2025 08:13:43 +0100 Subject: [PATCH 098/102] feat: introduce attribute (#802) --- docs/index.rst | 28 +++++--- src/Attribute/AsFoundryHook.php | 28 ++++++++ src/Object/Event/AfterInstantiate.php | 13 +++- src/Object/Event/BeforeInstantiate.php | 14 +++- src/Object/Event/Event.php | 25 +++++++ src/Object/Event/HookListenerFilter.php | 45 ++++++++++++ src/Persistence/Event/AfterPersist.php | 14 +++- src/ZenstruckFoundryBundle.php | 40 +++++++++++ tests/Fixture/Events/FoundryEventListener.php | 72 ++++++++++++++++++- .../{Persistence => }/EventsTest.php | 9 ++- 10 files changed, 265 insertions(+), 23 deletions(-) create mode 100644 src/Attribute/AsFoundryHook.php create mode 100644 src/Object/Event/Event.php create mode 100644 src/Object/Event/HookListenerFilter.php rename tests/Integration/{Persistence => }/EventsTest.php (74%) diff --git a/docs/index.rst b/docs/index.rst index 7921a68c4..f064f1294 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -603,11 +603,11 @@ You can also add hooks directly in your factory class: Read `Initialization`_ to learn more about the ``initialize()`` method. -Events -~~~~~~ +Hooks as service / global hooks +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -In addition to hooks, Foundry also leverages `symfony/event-dispatcher` and dispatches events that you can listen to, -allowing to create hooks globally, as Symfony services: +For a better control of your hooks, you can define them as services, allowing to leverage dependency injection and +to create hooks globally: :: @@ -616,26 +616,26 @@ allowing to create hooks globally, as Symfony services: use Zenstruck\Foundry\Object\Event\BeforeInstantiate; use Zenstruck\Foundry\Persistence\Event\AfterPersist; - final class FoundryEventListener + final class FoundryHook { - #[AsEventListener] + #[AsFoundryHook(Post::class)] public function beforeInstantiate(BeforeInstantiate $event): void { - // do something before the object is instantiated: + // do something before the post is instantiated: // $event->parameters is what will be used to instantiate the object, manipulate as required // $event->objectClass is the class of the object being instantiated // $event->factory is the factory instance which creates the object } - #[AsEventListener] + #[AsFoundryHook(Post::class)] public function afterInstantiate(AfterInstantiate $event): void { - // $event->object is the instantiated object + // $event->object is the instantiated Post object // $event->parameters contains the attributes used to instantiate the object and any extras // $event->factory is the factory instance which creates the object } - #[AsEventListener] + #[AsFoundryHook(Post::class)] public function afterPersist(AfterPersist $event): void { // this event is only called if the object was persisted @@ -643,11 +643,17 @@ allowing to create hooks globally, as Symfony services: // $event->parameters contains the attributes used to instantiate the object and any extras // $event->factory is the factory instance which creates the object } + + #[AsFoundryHook] + public function afterInstantiateGlobal(AfterInstantiate $event): void + { + // Omitting class defines a "global" hook which will be called for all objects + } } .. versionadded:: 2.4 - Those events are triggered since Foundry 2.4. + The ``#[AsFoundryHook]`` attribute was added in Foundry 2.4. Initialization ~~~~~~~~~~~~~~ diff --git a/src/Attribute/AsFoundryHook.php b/src/Attribute/AsFoundryHook.php new file mode 100644 index 000000000..68ff14e16 --- /dev/null +++ b/src/Attribute/AsFoundryHook.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Attribute; + +use Symfony\Component\EventDispatcher\Attribute\AsEventListener; + +#[\Attribute(\Attribute::TARGET_METHOD)] +final class AsFoundryHook extends AsEventListener +{ + public function __construct( + /** @var class-string */ + public readonly ?string $objectClass = null, + int $priority = 0, + ) { + parent::__construct(priority: $priority); + } +} diff --git a/src/Object/Event/AfterInstantiate.php b/src/Object/Event/AfterInstantiate.php index 7bbef74f5..356256531 100644 --- a/src/Object/Event/AfterInstantiate.php +++ b/src/Object/Event/AfterInstantiate.php @@ -19,16 +19,25 @@ /** * @author Nicolas PHILIPPE * + * @template T of object + * @implements Event + * * @phpstan-import-type Parameters from Factory */ -final class AfterInstantiate +final class AfterInstantiate implements Event { public function __construct( + /** @var T */ public readonly object $object, /** @phpstan-var Parameters */ public readonly array $parameters, - /** @var ObjectFactory */ + /** @var ObjectFactory */ public readonly ObjectFactory $factory, ) { } + + public function objectClassName(): string + { + return $this->object::class; + } } diff --git a/src/Object/Event/BeforeInstantiate.php b/src/Object/Event/BeforeInstantiate.php index b5ad5cb38..a79174056 100644 --- a/src/Object/Event/BeforeInstantiate.php +++ b/src/Object/Event/BeforeInstantiate.php @@ -19,17 +19,25 @@ /** * @author Nicolas PHILIPPE * + * @template T of object + * @implements Event + * * @phpstan-import-type Parameters from Factory */ -final class BeforeInstantiate +final class BeforeInstantiate implements Event { public function __construct( /** @phpstan-var Parameters */ public array $parameters, - /** @var class-string */ + /** @var class-string */ public readonly string $objectClass, - /** @var ObjectFactory */ + /** @var ObjectFactory */ public readonly ObjectFactory $factory, ) { } + + public function objectClassName(): string + { + return $this->objectClass; + } } diff --git a/src/Object/Event/Event.php b/src/Object/Event/Event.php new file mode 100644 index 000000000..95382cb25 --- /dev/null +++ b/src/Object/Event/Event.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Object\Event; + +/** + * @template T of object + */ +interface Event +{ + /** + * @return class-string + */ + public function objectClassName(): string; +} diff --git a/src/Object/Event/HookListenerFilter.php b/src/Object/Event/HookListenerFilter.php new file mode 100644 index 000000000..12e28e0eb --- /dev/null +++ b/src/Object/Event/HookListenerFilter.php @@ -0,0 +1,45 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Object\Event; + +final class HookListenerFilter +{ + /** @var \Closure(Event): void */ + private \Closure $listener; + + /** + * @param array{0: object, 1: string} $listener + * @param class-string|null $objectClass + */ + public function __construct(array $listener, private ?string $objectClass = null) + { + if (!\is_callable($listener)) { + throw new \InvalidArgumentException(\sprintf('Listener must be a callable, "%s" given.', \get_debug_type($listener))); + } + + $this->listener = $listener(...); + } + + /** + * @param Event $event + */ + public function __invoke(Event $event): void + { + if ($this->objectClass && $event->objectClassName() !== $this->objectClass) { + return; + } + + ($this->listener)($event); + } +} diff --git a/src/Persistence/Event/AfterPersist.php b/src/Persistence/Event/AfterPersist.php index eb89763c2..6ab3fa293 100644 --- a/src/Persistence/Event/AfterPersist.php +++ b/src/Persistence/Event/AfterPersist.php @@ -14,21 +14,31 @@ namespace Zenstruck\Foundry\Persistence\Event; use Zenstruck\Foundry\Factory; +use Zenstruck\Foundry\Object\Event\Event; use Zenstruck\Foundry\Persistence\PersistentObjectFactory; /** * @author Nicolas PHILIPPE * + * @template T of object + * @implements Event + * * @phpstan-import-type Parameters from Factory */ -final class AfterPersist +final class AfterPersist implements Event { public function __construct( + /** @var T */ public readonly object $object, /** @phpstan-var Parameters */ public readonly array $parameters, - /** @var PersistentObjectFactory */ + /** @var PersistentObjectFactory */ public readonly PersistentObjectFactory $factory, ) { } + + public function objectClassName(): string + { + return $this->object::class; + } } diff --git a/src/ZenstruckFoundryBundle.php b/src/ZenstruckFoundryBundle.php index 52a55e91f..3036897c4 100644 --- a/src/ZenstruckFoundryBundle.php +++ b/src/ZenstruckFoundryBundle.php @@ -12,12 +12,18 @@ namespace Zenstruck\Foundry; use Symfony\Component\Config\Definition\Configurator\DefinitionConfigurator; +use Symfony\Component\DependencyInjection\ChildDefinition; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\Exception\LogicException; use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\HttpKernel\Bundle\AbstractBundle; +use Zenstruck\Foundry\Attribute\AsFoundryHook; use Zenstruck\Foundry\Mongo\MongoResetter; +use Zenstruck\Foundry\Object\Event\Event; +use Zenstruck\Foundry\Object\Event\HookListenerFilter; use Zenstruck\Foundry\Object\Instantiator; use Zenstruck\Foundry\ORM\ResetDatabase\MigrateDatabaseResetter; use Zenstruck\Foundry\ORM\ResetDatabase\OrmResetter; @@ -282,6 +288,25 @@ public function loadExtension(array $config, ContainerConfigurator $configurator ->replaceArgument(0, $config['mongo']['reset']['document_managers']) ; } + + $container->registerAttributeForAutoconfiguration( + AsFoundryHook::class, + // @phpstan-ignore argument.type + static function(ChildDefinition $definition, AsFoundryHook $attribute, \ReflectionMethod $reflector) { + if (1 !== \count($reflector->getParameters()) + || !$reflector->getParameters()[0]->getType() + || !$reflector->getParameters()[0]->getType() instanceof \ReflectionNamedType + || !\is_a($reflector->getParameters()[0]->getType()->getName(), Event::class, true) + ) { + throw new LogicException(\sprintf("In order to use \"%s\" attribute, method \"{$reflector->class}::{$reflector->name}()\" must have a single parameter that is a subclass of \"%s\".", AsFoundryHook::class, Event::class)); + } + $definition->addTag('foundry.hook', [ + 'class' => $attribute->objectClass, + 'method' => $reflector->getName(), + 'event' => $reflector->getParameters()[0]->getType()->getName(), + ]); + } + ); } public function build(ContainerBuilder $container): void @@ -300,6 +325,21 @@ public function process(ContainerBuilder $container): void ->addMethodCall('addProvider', [new Reference($id)]) ; } + + // events + $i = 0; + foreach ($container->findTaggedServiceIds('foundry.hook') as $id => $tags) { + foreach ($tags as $tag) { + $container + ->setDefinition("foundry.hook.{$tag['event']}.{$i}", new Definition(class: HookListenerFilter::class)) + ->setArgument(0, [new Reference($id), $tag['method']]) + ->setArgument(1, $tag['class']) + ->addTag('kernel.event_listener', ['event' => $tag['event']]) + ; + + ++$i; + } + } } /** diff --git a/tests/Fixture/Events/FoundryEventListener.php b/tests/Fixture/Events/FoundryEventListener.php index ddc00e4f0..c9df24006 100644 --- a/tests/Fixture/Events/FoundryEventListener.php +++ b/tests/Fixture/Events/FoundryEventListener.php @@ -14,13 +14,16 @@ namespace Zenstruck\Foundry\Tests\Fixture\Events; use Symfony\Component\EventDispatcher\Attribute\AsEventListener; +use Zenstruck\Foundry\Attribute\AsFoundryHook; use Zenstruck\Foundry\Object\Event\AfterInstantiate; use Zenstruck\Foundry\Object\Event\BeforeInstantiate; +use Zenstruck\Foundry\Object\Event\Event; use Zenstruck\Foundry\Persistence\Event\AfterPersist; use Zenstruck\Foundry\Tests\Fixture\Entity\EntityForEventListeners; final class FoundryEventListener { + /** @param BeforeInstantiate $event */ #[AsEventListener] public function beforeInstantiate(BeforeInstantiate $event): void { @@ -28,9 +31,10 @@ public function beforeInstantiate(BeforeInstantiate $event): void return; } - $event->parameters['name'] = "{$event->parameters['name']}\nBeforeInstantiate"; + $event->parameters['name'] = $this->name($event->parameters['name'], $event); } + /** @param AfterInstantiate $event */ #[AsEventListener] public function afterInstantiate(AfterInstantiate $event): void { @@ -38,9 +42,10 @@ public function afterInstantiate(AfterInstantiate $event): void return; } - $event->object->name = "{$event->object->name}\nAfterInstantiate"; + $event->object->name = $this->name($event->object->name, $event); } + /** @param AfterPersist $event */ #[AsEventListener] public function afterPersist(AfterPersist $event): void { @@ -48,6 +53,67 @@ public function afterPersist(AfterPersist $event): void return; } - $event->object->name = "{$event->object->name}\nAfterPersist"; + $event->object->name = $this->name($event->object->name, $event); + } + + /** @param BeforeInstantiate $event */ + #[AsFoundryHook(EntityForEventListeners::class)] + public function beforeInstantiateWithFoundryAttribute(BeforeInstantiate $event): void + { + $event->parameters['name'] = "{$this->name($event->parameters['name'], $event)} with Foundry attribute"; + } + + /** @param AfterInstantiate $event */ + #[AsFoundryHook(EntityForEventListeners::class)] + public function afterInstantiateWithFoundryAttribute(AfterInstantiate $event): void + { + $event->object->name = "{$this->name($event->object->name, $event)} with Foundry attribute"; + } + + /** @param AfterPersist $event */ + #[AsFoundryHook(EntityForEventListeners::class)] + public function afterPersistWithFoundryAttribute(AfterPersist $event): void + { + $event->object->name = "{$this->name($event->object->name, $event)} with Foundry attribute"; + } + + /** @param BeforeInstantiate $event */ + #[AsFoundryHook()] + public function globalBeforeInstantiate(BeforeInstantiate $event): void + { + if (EntityForEventListeners::class !== $event->objectClass) { + return; + } + + $event->parameters['name'] = "{$this->name($event->parameters['name'], $event)} global"; + } + + /** @param AfterInstantiate $event */ + #[AsFoundryHook()] + public function globalAfterInstantiate(AfterInstantiate $event): void + { + if (!$event->object instanceof EntityForEventListeners) { + return; + } + + $event->object->name = "{$this->name($event->object->name, $event)} global"; + } + + /** @param AfterPersist $event */ + #[AsFoundryHook()] + public function globalAfterPersist(AfterPersist $event): void + { + if (!$event->object instanceof EntityForEventListeners) { + return; + } + + $event->object->name = "{$this->name($event->object->name, $event)} global"; + } + + private function name(string $name, Event $event): string // @phpstan-ignore missingType.generics + { + $eventName = (new \ReflectionClass($event))->getShortName(); + + return "{$name}\n{$eventName}"; } } diff --git a/tests/Integration/Persistence/EventsTest.php b/tests/Integration/EventsTest.php similarity index 74% rename from tests/Integration/Persistence/EventsTest.php rename to tests/Integration/EventsTest.php index 358d0f3bc..25398d187 100644 --- a/tests/Integration/Persistence/EventsTest.php +++ b/tests/Integration/EventsTest.php @@ -11,13 +11,12 @@ * file that was distributed with this source code. */ -namespace Zenstruck\Foundry\Tests\Integration\Persistence; +namespace Zenstruck\Foundry\Tests\Integration; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; use Zenstruck\Foundry\Test\Factories; use Zenstruck\Foundry\Test\ResetDatabase; use Zenstruck\Foundry\Tests\Fixture\Events\FactoryWithEventListeners; -use Zenstruck\Foundry\Tests\Integration\RequiresORM; final class EventsTest extends KernelTestCase { @@ -34,8 +33,14 @@ public function it_can_call_hooks(): void <<name ); From ad8d72c069bfd79e7e93c517941a52edb6df5b3c Mon Sep 17 00:00:00 2001 From: Nicolas PHILIPPE Date: Fri, 14 Feb 2025 07:06:46 +0100 Subject: [PATCH 099/102] fix: can index one to many relationships based on "indexBy" (#815) --- src/ORM/OrmV2PersistenceStrategy.php | 1 + src/ORM/OrmV3PersistenceStrategy.php | 1 + .../InverseRelationshipMetadata.php | 1 + src/Persistence/PersistentObjectFactory.php | 18 +++++-- .../EdgeCases/IndexedOneToMany/Child.php | 26 ++++++++++ .../IndexedOneToMany/ParentEntity.php | 49 +++++++++++++++++++ .../ORM/EdgeCasesRelationshipTest.php | 24 +++++++++ 7 files changed, 117 insertions(+), 3 deletions(-) create mode 100644 tests/Fixture/Entity/EdgeCases/IndexedOneToMany/Child.php create mode 100644 tests/Fixture/Entity/EdgeCases/IndexedOneToMany/ParentEntity.php diff --git a/src/ORM/OrmV2PersistenceStrategy.php b/src/ORM/OrmV2PersistenceStrategy.php index 2a1d14fd5..7e2ae2a2c 100644 --- a/src/ORM/OrmV2PersistenceStrategy.php +++ b/src/ORM/OrmV2PersistenceStrategy.php @@ -66,6 +66,7 @@ public function inversedRelationshipMetadata(string $parent, string $child, stri return new InverseRelationshipMetadata( inverseField: $association['fieldName'], isCollection: $inversedAssociationMetadata->isCollectionValuedAssociation($inversedAssociation['fieldName']), + collectionIndexedBy: $inversedAssociation['indexBy'] ?? null ); } diff --git a/src/ORM/OrmV3PersistenceStrategy.php b/src/ORM/OrmV3PersistenceStrategy.php index 6d8d187e3..e105b176c 100644 --- a/src/ORM/OrmV3PersistenceStrategy.php +++ b/src/ORM/OrmV3PersistenceStrategy.php @@ -56,6 +56,7 @@ public function inversedRelationshipMetadata(string $parent, string $child, stri return new InverseRelationshipMetadata( inverseField: $association->fieldName, isCollection: $inversedAssociation instanceof ToManyAssociationMapping, + collectionIndexedBy: $inversedAssociation->isIndexed() ? $inversedAssociation->indexBy() : null ); } diff --git a/src/Persistence/InverseRelationshipMetadata.php b/src/Persistence/InverseRelationshipMetadata.php index 50b35c9ec..fe153af0c 100644 --- a/src/Persistence/InverseRelationshipMetadata.php +++ b/src/Persistence/InverseRelationshipMetadata.php @@ -21,6 +21,7 @@ final class InverseRelationshipMetadata public function __construct( public readonly string $inverseField, public readonly bool $isCollection, + public readonly string|null $collectionIndexedBy, ) { } } diff --git a/src/Persistence/PersistentObjectFactory.php b/src/Persistence/PersistentObjectFactory.php index 7267096c9..6fce73bd2 100644 --- a/src/Persistence/PersistentObjectFactory.php +++ b/src/Persistence/PersistentObjectFactory.php @@ -22,6 +22,7 @@ use Zenstruck\Foundry\Persistence\Exception\NotEnoughObjects; use Zenstruck\Foundry\Persistence\Exception\RefreshObjectFailed; +use function Zenstruck\Foundry\get; use function Zenstruck\Foundry\set; /** @@ -335,11 +336,22 @@ protected function normalizeCollection(string $field, FactoryCollection $collect $inverseRelationshipMetadata = $pm->inverseRelationshipMetadata(static::class(), $collection->factory::class(), $field); if ($inverseRelationshipMetadata && $inverseRelationshipMetadata->isCollection) { - $inverseField = $inverseRelationshipMetadata->inverseField; + $this->tempAfterInstantiate[] = static function(object $object) use ($collection, $inverseRelationshipMetadata, $field) { + $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)); + + $inverseObjects = unproxy($inverseObjects); + + // if the collection is indexed by a field, index the array + if ($inverseRelationshipMetadata->collectionIndexedBy) { + $inverseObjects = \array_combine( + array_map(static fn($o) => get($o, $inverseRelationshipMetadata->collectionIndexedBy), $inverseObjects), + array_values($inverseObjects) + ); + } + + set($object, $field, $inverseObjects); }; // creation delegated to afterPersist hook - return empty array here diff --git a/tests/Fixture/Entity/EdgeCases/IndexedOneToMany/Child.php b/tests/Fixture/Entity/EdgeCases/IndexedOneToMany/Child.php new file mode 100644 index 000000000..8850aec65 --- /dev/null +++ b/tests/Fixture/Entity/EdgeCases/IndexedOneToMany/Child.php @@ -0,0 +1,26 @@ + + */ +#[ORM\Entity] +#[ORM\Table('index_by_one_to_many_level_1')] +class Child extends Base +{ + public function __construct( + #[ORM\ManyToOne(inversedBy: 'items')] + #[ORM\JoinColumn(nullable: false)] + public ParentEntity $parent, + + #[ORM\Column()] + public string $language, + ) { + } +} diff --git a/tests/Fixture/Entity/EdgeCases/IndexedOneToMany/ParentEntity.php b/tests/Fixture/Entity/EdgeCases/IndexedOneToMany/ParentEntity.php new file mode 100644 index 000000000..cbacab2b7 --- /dev/null +++ b/tests/Fixture/Entity/EdgeCases/IndexedOneToMany/ParentEntity.php @@ -0,0 +1,49 @@ + + */ +#[ORM\Entity] +#[ORM\Table('index_by_one_to_many_parent')] +class ParentEntity extends Base +{ + /** @var Collection */ + #[ORM\OneToMany(targetEntity: Child::class, mappedBy: 'parent', indexBy: 'language')] + private Collection $items; + + public function __construct() + { + $this->items = new ArrayCollection(); + } + + /** + * @return Collection + */ + public function getItems(): Collection + { + return $this->items; + } + + public function addItem(Child $item): void + { + if (!$this->items->contains($item)) { + $this->items->add($item); + } + } + + public function removeItem(Child $item): void + { + if ($this->items->contains($item)) { + $this->items->removeElement($item); + } + } +} diff --git a/tests/Integration/ORM/EdgeCasesRelationshipTest.php b/tests/Integration/ORM/EdgeCasesRelationshipTest.php index 27a94e13c..c2644350c 100644 --- a/tests/Integration/ORM/EdgeCasesRelationshipTest.php +++ b/tests/Integration/ORM/EdgeCasesRelationshipTest.php @@ -21,11 +21,13 @@ 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\IndexedOneToMany\Level1Factory; use Zenstruck\Foundry\Tests\Fixture\Entity\EdgeCases\InversedOneToOneWithNonNullableOwning; use Zenstruck\Foundry\Tests\Fixture\Entity\EdgeCases\InversedOneToOneWithOneToMany; use Zenstruck\Foundry\Tests\Fixture\Entity\EdgeCases\InversedOneToOneWithSetter; use Zenstruck\Foundry\Tests\Fixture\Entity\EdgeCases\ManyToOneToSelfReferencing; use Zenstruck\Foundry\Tests\Fixture\Entity\EdgeCases\RichDomainMandatoryRelationship; +use Zenstruck\Foundry\Tests\Fixture\Entity\EdgeCases\IndexedOneToMany; use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\EdgeCases\MultipleMandatoryRelationshipToSameEntity; use Zenstruck\Foundry\Tests\Integration\RequiresORM; @@ -135,6 +137,28 @@ public function many_to_many_to_self_referencing_inverse_side(): void $inverseSideFactory::assert()->count(1); } + /** @test */ + #[Test] + #[DataProvider('provideCascadeRelationshipsCombinations')] + #[UsingRelationships(IndexedOneToMany\ParentEntity::class, ['items'])] + #[RequiresPhpunit('^11.4')] + public function indexed_one_to_many(): void + { + $parentFactory = persistent_factory(IndexedOneToMany\ParentEntity::class); + $childFactory = persistent_factory(IndexedOneToMany\Child::class); + + $parent = $parentFactory->create( + [ + 'items' => $childFactory->with(['language' => 'en', 'parent' => $parentFactory])->many(1), + ] + ); + + $parentFactory::assert()->count(1); + $childFactory::assert()->count(1); + + self::assertNotNull($parent->getItems()->get('en')); // @phpstan-ignore argument.type + } + /** * @test */ From f9e17aebfdfa9415975f02929fd54c512bcc8724 Mon Sep 17 00:00:00 2001 From: nikophil <10139766+nikophil@users.noreply.github.com> Date: Fri, 14 Feb 2025 06:09:38 +0000 Subject: [PATCH 100/102] bot: fix cs [skip ci] --- src/Persistence/InverseRelationshipMetadata.php | 2 +- src/Persistence/PersistentObjectFactory.php | 4 ++-- .../Fixture/Entity/EdgeCases/IndexedOneToMany/Child.php | 9 +++++++++ .../Entity/EdgeCases/IndexedOneToMany/ParentEntity.php | 9 +++++++++ tests/Integration/ORM/EdgeCasesRelationshipTest.php | 3 +-- 5 files changed, 22 insertions(+), 5 deletions(-) diff --git a/src/Persistence/InverseRelationshipMetadata.php b/src/Persistence/InverseRelationshipMetadata.php index fe153af0c..1287b8f56 100644 --- a/src/Persistence/InverseRelationshipMetadata.php +++ b/src/Persistence/InverseRelationshipMetadata.php @@ -21,7 +21,7 @@ final class InverseRelationshipMetadata public function __construct( public readonly string $inverseField, public readonly bool $isCollection, - public readonly string|null $collectionIndexedBy, + public readonly ?string $collectionIndexedBy, ) { } } diff --git a/src/Persistence/PersistentObjectFactory.php b/src/Persistence/PersistentObjectFactory.php index 6fce73bd2..825dca681 100644 --- a/src/Persistence/PersistentObjectFactory.php +++ b/src/Persistence/PersistentObjectFactory.php @@ -346,8 +346,8 @@ protected function normalizeCollection(string $field, FactoryCollection $collect // if the collection is indexed by a field, index the array if ($inverseRelationshipMetadata->collectionIndexedBy) { $inverseObjects = \array_combine( - array_map(static fn($o) => get($o, $inverseRelationshipMetadata->collectionIndexedBy), $inverseObjects), - array_values($inverseObjects) + \array_map(static fn($o) => get($o, $inverseRelationshipMetadata->collectionIndexedBy), $inverseObjects), + \array_values($inverseObjects) ); } diff --git a/tests/Fixture/Entity/EdgeCases/IndexedOneToMany/Child.php b/tests/Fixture/Entity/EdgeCases/IndexedOneToMany/Child.php index 8850aec65..0bcf49c12 100644 --- a/tests/Fixture/Entity/EdgeCases/IndexedOneToMany/Child.php +++ b/tests/Fixture/Entity/EdgeCases/IndexedOneToMany/Child.php @@ -2,6 +2,15 @@ declare(strict_types=1); +/* + * This file is part of the zenstruck/foundry package. + * + * (c) Kevin Bond + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Zenstruck\Foundry\Tests\Fixture\Entity\EdgeCases\IndexedOneToMany; use Doctrine\ORM\Mapping as ORM; diff --git a/tests/Fixture/Entity/EdgeCases/IndexedOneToMany/ParentEntity.php b/tests/Fixture/Entity/EdgeCases/IndexedOneToMany/ParentEntity.php index cbacab2b7..4ddc7b58d 100644 --- a/tests/Fixture/Entity/EdgeCases/IndexedOneToMany/ParentEntity.php +++ b/tests/Fixture/Entity/EdgeCases/IndexedOneToMany/ParentEntity.php @@ -2,6 +2,15 @@ declare(strict_types=1); +/* + * This file is part of the zenstruck/foundry package. + * + * (c) Kevin Bond + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Zenstruck\Foundry\Tests\Fixture\Entity\EdgeCases\IndexedOneToMany; use Doctrine\Common\Collections\ArrayCollection; diff --git a/tests/Integration/ORM/EdgeCasesRelationshipTest.php b/tests/Integration/ORM/EdgeCasesRelationshipTest.php index c2644350c..5605c2f1b 100644 --- a/tests/Integration/ORM/EdgeCasesRelationshipTest.php +++ b/tests/Integration/ORM/EdgeCasesRelationshipTest.php @@ -21,13 +21,12 @@ 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\IndexedOneToMany\Level1Factory; +use Zenstruck\Foundry\Tests\Fixture\Entity\EdgeCases\IndexedOneToMany; use Zenstruck\Foundry\Tests\Fixture\Entity\EdgeCases\InversedOneToOneWithNonNullableOwning; use Zenstruck\Foundry\Tests\Fixture\Entity\EdgeCases\InversedOneToOneWithOneToMany; use Zenstruck\Foundry\Tests\Fixture\Entity\EdgeCases\InversedOneToOneWithSetter; use Zenstruck\Foundry\Tests\Fixture\Entity\EdgeCases\ManyToOneToSelfReferencing; use Zenstruck\Foundry\Tests\Fixture\Entity\EdgeCases\RichDomainMandatoryRelationship; -use Zenstruck\Foundry\Tests\Fixture\Entity\EdgeCases\IndexedOneToMany; use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\EdgeCases\MultipleMandatoryRelationshipToSameEntity; use Zenstruck\Foundry\Tests\Integration\RequiresORM; From 19d4f79767fa6505ad4092cdb9542de0a747cd85 Mon Sep 17 00:00:00 2001 From: Nicolas PHILIPPE Date: Fri, 14 Feb 2025 12:35:54 +0100 Subject: [PATCH 101/102] changelog: update [skip ci] --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index cabfeb842..a977db501 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # CHANGELOG +## [v2.3.4](https://github.com/zenstruck/foundry/releases/tag/v2.3.4) + +February 14th, 2025 - [v2.3.3...v2.3.4](https://github.com/zenstruck/foundry/compare/v2.3.3...v2.3.4) + +* ad8d72c fix: can index one to many relationships based on "indexBy" (#815) by @nikophil + ## [v2.3.2](https://github.com/zenstruck/foundry/releases/tag/v2.3.2) February 1st, 2025 - [v2.3.1...v2.3.2](https://github.com/zenstruck/foundry/compare/v2.3.1...v2.3.2) From 5cb5d9a657f03455e918e53e6a66dbeaea4144ca Mon Sep 17 00:00:00 2001 From: Nicolas PHILIPPE Date: Sun, 18 Aug 2024 19:21:45 +0200 Subject: [PATCH 102/102] feat: introduce "in-memory" behavior --- config/in_memory.php | 18 ++ config/services.php | 1 + src/Configuration.php | 24 ++- src/Exception/CannotCreateFactory.php | 26 +++ src/Factory.php | 3 +- src/FactoryRegistry.php | 19 +- src/FactoryRegistryInterface.php | 29 ++++ src/InMemory/AsInMemoryTest.php | 36 ++++ src/InMemory/CannotEnableInMemory.php | 27 +++ .../InMemoryCompilerPass.php | 55 ++++++ src/InMemory/GenericInMemoryRepository.php | 61 +++++++ src/InMemory/InMemoryFactoryRegistry.php | 59 +++++++ src/InMemory/InMemoryRepository.php | 37 ++++ src/InMemory/InMemoryRepositoryRegistry.php | 50 ++++++ src/InMemory/InMemoryRepositoryTrait.php | 50 ++++++ .../BootFoundryOnDataProviderMethodCalled.php | 8 + src/PHPUnit/EnableInMemoryBeforeTest.php | 44 +++++ src/PHPUnit/FoundryExtension.php | 2 +- src/Persistence/PersistentObjectFactory.php | 4 +- src/ZenstruckFoundryBundle.php | 7 + .../InMemory/InMemoryAddressRepository.php | 32 ++++ .../InMemory/InMemoryContactRepository.php | 32 ++++ tests/Fixture/TestKernel.php | 4 + .../DataProviderWithInMemoryTest.php | 99 +++++++++++ .../InMemoryAttributeOnMethodTest.php | 77 +++++++++ tests/Integration/InMemory/InMemoryTest.php | 162 ++++++++++++++++++ 26 files changed, 951 insertions(+), 15 deletions(-) create mode 100644 config/in_memory.php create mode 100644 src/Exception/CannotCreateFactory.php create mode 100644 src/FactoryRegistryInterface.php create mode 100644 src/InMemory/AsInMemoryTest.php create mode 100644 src/InMemory/CannotEnableInMemory.php create mode 100644 src/InMemory/DependencyInjection/InMemoryCompilerPass.php create mode 100644 src/InMemory/GenericInMemoryRepository.php create mode 100644 src/InMemory/InMemoryFactoryRegistry.php create mode 100644 src/InMemory/InMemoryRepository.php create mode 100644 src/InMemory/InMemoryRepositoryRegistry.php create mode 100644 src/InMemory/InMemoryRepositoryTrait.php create mode 100644 src/PHPUnit/EnableInMemoryBeforeTest.php create mode 100644 tests/Fixture/InMemory/InMemoryAddressRepository.php create mode 100644 tests/Fixture/InMemory/InMemoryContactRepository.php create mode 100644 tests/Integration/DataProvider/DataProviderWithInMemoryTest.php create mode 100644 tests/Integration/InMemory/InMemoryAttributeOnMethodTest.php create mode 100644 tests/Integration/InMemory/InMemoryTest.php diff --git a/config/in_memory.php b/config/in_memory.php new file mode 100644 index 000000000..2c302988f --- /dev/null +++ b/config/in_memory.php @@ -0,0 +1,18 @@ +services() + ->set('.zenstruck_foundry.in_memory.factory_registry', InMemoryFactoryRegistry::class) + ->decorate('.zenstruck_foundry.factory_registry') + ->arg('$decorated', service('.inner')); + + $container->services() + ->set('.zenstruck_foundry.in_memory.repository_registry', InMemoryRepositoryRegistry::class) + ->arg('$inMemoryRepositories', abstract_arg('inMemoryRepositories')) + ; +}; diff --git a/config/services.php b/config/services.php index 94b92aa01..7c1b16e9e 100644 --- a/config/services.php +++ b/config/services.php @@ -33,6 +33,7 @@ service('.zenstruck_foundry.story_registry'), service('.zenstruck_foundry.persistence_manager')->nullOnInvalid(), service('event_dispatcher'), + service('.zenstruck_foundry.in_memory.repository_registry'), ]) ->public() ; diff --git a/src/Configuration.php b/src/Configuration.php index 3bf880287..dcf436496 100644 --- a/src/Configuration.php +++ b/src/Configuration.php @@ -17,6 +17,8 @@ use Zenstruck\Foundry\Exception\FoundryNotBooted; use Zenstruck\Foundry\Exception\PersistenceDisabled; use Zenstruck\Foundry\Exception\PersistenceNotAvailable; +use Zenstruck\Foundry\InMemory\CannotEnableInMemory; +use Zenstruck\Foundry\InMemory\InMemoryRepositoryRegistry; use Zenstruck\Foundry\Persistence\PersistenceManager; /** @@ -43,16 +45,19 @@ final class Configuration /** @var \Closure():self|self|null */ private static \Closure|self|null $instance = null; + private bool $inMemory = false; + /** * @phpstan-param InstantiatorCallable $instantiator */ public function __construct( - public readonly FactoryRegistry $factories, + public readonly FactoryRegistryInterface $factories, public readonly Faker\Generator $faker, callable $instantiator, public readonly StoryRegistry $stories, private readonly ?PersistenceManager $persistence = null, private readonly ?EventDispatcherInterface $eventDispatcher = null, + public readonly ?InMemoryRepositoryRegistry $inMemoryRepositoryRegistry = null, ) { $this->instantiator = $instantiator; } @@ -131,4 +136,21 @@ public static function shutdown(): void StoryRegistry::reset(); self::$instance = null; } + + /** + * @throws CannotEnableInMemory + */ + public function enableInMemory(): void + { + if (null === $this->inMemoryRepositoryRegistry) { + throw CannotEnableInMemory::noInMemoryRepositoryRegistry(); + } + + $this->inMemory = true; + } + + public function isInMemoryEnabled(): bool + { + return $this->inMemory; + } } diff --git a/src/Exception/CannotCreateFactory.php b/src/Exception/CannotCreateFactory.php new file mode 100644 index 000000000..3845f1310 --- /dev/null +++ b/src/Exception/CannotCreateFactory.php @@ -0,0 +1,26 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Exception; + +/** + * @author Nicolas PHILIPPE + * @internal + */ +final class CannotCreateFactory extends \LogicException +{ + public static function argumentCountError(\ArgumentCountError $e): static + { + return new self('Factories with dependencies (services) cannot be created before foundry is booted.', previous: $e); + } +} diff --git a/src/Factory.php b/src/Factory.php index c4489582a..3344e002f 100644 --- a/src/Factory.php +++ b/src/Factory.php @@ -12,6 +12,7 @@ namespace Zenstruck\Foundry; use Faker; +use Zenstruck\Foundry\Exception\CannotCreateFactory; /** * @author Kevin Bond @@ -53,7 +54,7 @@ final public static function new(array|callable $attributes = []): static try { $factory ??= new static(); // @phpstan-ignore new.static } catch (\ArgumentCountError $e) { - throw new \LogicException('Factories with dependencies (services) cannot be created before foundry is booted.', previous: $e); + throw CannotCreateFactory::argumentCountError($e); } return $factory diff --git a/src/FactoryRegistry.php b/src/FactoryRegistry.php index 2a554e2e5..6c1188248 100644 --- a/src/FactoryRegistry.php +++ b/src/FactoryRegistry.php @@ -11,12 +11,14 @@ namespace Zenstruck\Foundry; +use Zenstruck\Foundry\Exception\CannotCreateFactory; + /** * @author Kevin Bond * * @internal */ -final class FactoryRegistry +final class FactoryRegistry implements FactoryRegistryInterface { /** * @param Factory[] $factories @@ -25,14 +27,7 @@ public function __construct(private iterable $factories) { } - /** - * @template T of Factory - * - * @param class-string $class - * - * @return T|null - */ - public function get(string $class): ?Factory + public function get(string $class): Factory { foreach ($this->factories as $factory) { if ($class === $factory::class) { @@ -40,6 +35,10 @@ public function get(string $class): ?Factory } } - return null; + try { + return new $class(); + } catch (\ArgumentCountError $e) { + throw CannotCreateFactory::argumentCountError($e); + } } } diff --git a/src/FactoryRegistryInterface.php b/src/FactoryRegistryInterface.php new file mode 100644 index 000000000..f2e5a2026 --- /dev/null +++ b/src/FactoryRegistryInterface.php @@ -0,0 +1,29 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry; + +/** + * @author Nicolas PHILIPPE + * + * @internal + */ +interface FactoryRegistryInterface +{ + /** + * @template T of Factory + * + * @param class-string $class + * + * @return T + */ + public function get(string $class): Factory; +} diff --git a/src/InMemory/AsInMemoryTest.php b/src/InMemory/AsInMemoryTest.php new file mode 100644 index 000000000..2f91d551d --- /dev/null +++ b/src/InMemory/AsInMemoryTest.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\InMemory; + +/** + * @author Nicolas PHILIPPE + */ +#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD)] +final class AsInMemoryTest +{ + /** + * @param class-string $class + * @internal + */ + public static function shouldEnableInMemory(string $class, string $method): bool + { + $classReflection = new \ReflectionClass($class); + + if ($classReflection->getAttributes(static::class)) { + return true; + } + + return (bool) $classReflection->getMethod($method)->getAttributes(static::class); + } +} diff --git a/src/InMemory/CannotEnableInMemory.php b/src/InMemory/CannotEnableInMemory.php new file mode 100644 index 000000000..f272b7e09 --- /dev/null +++ b/src/InMemory/CannotEnableInMemory.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\InMemory; + +final class CannotEnableInMemory extends \LogicException +{ + public static function testIsNotAKernelTestCase(string $testName): self + { + return new self("{$testName}: Cannot use the #[AsInMemoryTest] attribute without extending KernelTestCase."); + } + + public static function noInMemoryRepositoryRegistry(): self + { + return new self('Cannot enable "in memory": maybe not in a KernelTestCase?'); + } +} diff --git a/src/InMemory/DependencyInjection/InMemoryCompilerPass.php b/src/InMemory/DependencyInjection/InMemoryCompilerPass.php new file mode 100644 index 000000000..bcdbe99eb --- /dev/null +++ b/src/InMemory/DependencyInjection/InMemoryCompilerPass.php @@ -0,0 +1,55 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\InMemory\DependencyInjection; + +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\Compiler\ServiceLocatorTagPass; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Reference; +use Zenstruck\Foundry\InMemory\InMemoryRepository; + +/** + * @internal + * @author Nicolas PHILIPPE + */ +final class InMemoryCompilerPass implements CompilerPassInterface +{ + public function process(ContainerBuilder $container): void + { + // create a service locator with all "in memory" repositories, indexed by target class + $inMemoryRepositoriesServices = $container->findTaggedServiceIds('foundry.in_memory.repository'); + $inMemoryRepositoriesLocator = ServiceLocatorTagPass::register( + $container, + \array_combine( + \array_map( + static function(string $serviceId) use ($container) { + /** @var class-string> $inMemoryRepositoryClass */ + $inMemoryRepositoryClass = $container->getDefinition($serviceId)->getClass() ?? throw new \LogicException("Service \"{$serviceId}\" should have a class."); + + return $inMemoryRepositoryClass::_class(); + }, + \array_keys($inMemoryRepositoriesServices) + ), + \array_map( + static fn(string $inMemoryRepositoryId) => new Reference($inMemoryRepositoryId), + \array_keys($inMemoryRepositoriesServices) + ), + ) + ); + + $container->findDefinition('.zenstruck_foundry.in_memory.repository_registry') + ->setArgument('$inMemoryRepositories', $inMemoryRepositoriesLocator) + ; + } +} diff --git a/src/InMemory/GenericInMemoryRepository.php b/src/InMemory/GenericInMemoryRepository.php new file mode 100644 index 000000000..9c97c2911 --- /dev/null +++ b/src/InMemory/GenericInMemoryRepository.php @@ -0,0 +1,61 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\InMemory; + +/** + * @template T of object + * @implements InMemoryRepository + * @author Nicolas PHILIPPE + * + * This class will be used when a specific "in-memory" repository does not exist for a given class. + */ +final class GenericInMemoryRepository implements InMemoryRepository +{ + /** + * @var list + */ + private array $elements = []; + + /** + * @param class-string $class + */ + public function __construct( + private readonly string $class, + ) { + } + + /** + * @param T $item + */ + public function _save(object $item): void + { + if (!$item instanceof $this->class) { + throw new \InvalidArgumentException(\sprintf('Given object of class "%s" is not an instance of expected "%s"', $item::class, $this->class)); + } + + if (!\in_array($item, $this->elements, true)) { + $this->elements[] = $item; + } + } + + public function _all(): array + { + return $this->elements; + } + + public static function _class(): string + { + throw new \BadMethodCallException('This method should not be called on a GenericInMemoryRepository.'); + } +} diff --git a/src/InMemory/InMemoryFactoryRegistry.php b/src/InMemory/InMemoryFactoryRegistry.php new file mode 100644 index 000000000..9a84667a5 --- /dev/null +++ b/src/InMemory/InMemoryFactoryRegistry.php @@ -0,0 +1,59 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\InMemory; + +use Zenstruck\Foundry\Configuration; +use Zenstruck\Foundry\Factory; +use Zenstruck\Foundry\FactoryRegistryInterface; +use Zenstruck\Foundry\ObjectFactory; +use Zenstruck\Foundry\Persistence\PersistentObjectFactory; + +/** + * @internal + * @author Nicolas PHILIPPE + */ +final class InMemoryFactoryRegistry implements FactoryRegistryInterface +{ + public function __construct( + private readonly FactoryRegistryInterface $decorated, + ) { + } + + /** + * @template T of Factory + * + * @param class-string $class + * + * @return T + */ + public function get(string $class): Factory + { + $factory = $this->decorated->get($class); + + if (!$factory instanceof ObjectFactory || !Configuration::instance()->isInMemoryEnabled()) { + return $factory; + } + + if ($factory instanceof PersistentObjectFactory) { + $factory = $factory->withoutPersisting(); + } + + return $factory // @phpstan-ignore argument.templateType + ->afterInstantiate( + function(object $object) use ($factory) { + Configuration::instance()->inMemoryRepositoryRegistry?->get($factory::class())->_save($object); + } + ); + } +} diff --git a/src/InMemory/InMemoryRepository.php b/src/InMemory/InMemoryRepository.php new file mode 100644 index 000000000..e9f4c6936 --- /dev/null +++ b/src/InMemory/InMemoryRepository.php @@ -0,0 +1,37 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\InMemory; + +/** + * @author Nicolas PHILIPPE + * + * @template T of object + */ +interface InMemoryRepository +{ + /** + * @return class-string + */ + public static function _class(): string; + + /** + * @param T $item + */ + public function _save(object $item): void; + + /** + * @return list + */ + public function _all(): array; +} diff --git a/src/InMemory/InMemoryRepositoryRegistry.php b/src/InMemory/InMemoryRepositoryRegistry.php new file mode 100644 index 000000000..982608c27 --- /dev/null +++ b/src/InMemory/InMemoryRepositoryRegistry.php @@ -0,0 +1,50 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\InMemory; + +use Symfony\Component\DependencyInjection\ServiceLocator; + +/** + * @internal + * @author Nicolas PHILIPPE + */ +final class InMemoryRepositoryRegistry +{ + /** + * @var array> + */ + private array $genericInMemoryRepositories = []; + + public function __construct( + /** @var ServiceLocator> */ + private readonly ServiceLocator $inMemoryRepositories, + ) { + } + + /** + * @template T of object + * + * @param class-string $class + * + * @return InMemoryRepository + */ + public function get(string $class): InMemoryRepository + { + if (!$this->inMemoryRepositories->has($class)) { + return $this->genericInMemoryRepositories[$class] ??= new GenericInMemoryRepository($class); // @phpstan-ignore return.type + } + + return $this->inMemoryRepositories->get($class); // @phpstan-ignore return.type + } +} diff --git a/src/InMemory/InMemoryRepositoryTrait.php b/src/InMemory/InMemoryRepositoryTrait.php new file mode 100644 index 000000000..edf1acb16 --- /dev/null +++ b/src/InMemory/InMemoryRepositoryTrait.php @@ -0,0 +1,50 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\InMemory; + +/** + * @template T of object + * @phpstan-require-implements InMemoryRepository + * + * @author Nicolas PHILIPPE + */ +trait InMemoryRepositoryTrait +{ + /** + * @var list + */ + private array $items = []; + + /** + * @param T $item + */ + public function _save(object $item): void + { + if (!\is_a($item, self::_class(), allow_string: true)) { + throw new \InvalidArgumentException(\sprintf('Given object of class "%s" is not an instance of expected "%s"', $item::class, self::_class())); + } + + if (!\in_array($item, $this->items, true)) { + $this->items[] = $item; + } + } + + /** + * @return list + */ + public function _all(): array + { + return $this->items; + } +} diff --git a/src/PHPUnit/BootFoundryOnDataProviderMethodCalled.php b/src/PHPUnit/BootFoundryOnDataProviderMethodCalled.php index 4564efb68..8f58ca6bb 100644 --- a/src/PHPUnit/BootFoundryOnDataProviderMethodCalled.php +++ b/src/PHPUnit/BootFoundryOnDataProviderMethodCalled.php @@ -14,6 +14,8 @@ namespace Zenstruck\Foundry\PHPUnit; use PHPUnit\Event; +use Zenstruck\Foundry\Configuration; +use Zenstruck\Foundry\InMemory\AsInMemoryTest; /** * @internal @@ -26,5 +28,11 @@ public function notify(Event\Test\DataProviderMethodCalled $event): void if (\method_exists($event->testMethod()->className(), '_bootForDataProvider')) { $event->testMethod()->className()::_bootForDataProvider(); } + + $testMethod = $event->testMethod(); + + if (AsInMemoryTest::shouldEnableInMemory($testMethod->className(), $testMethod->methodName())) { + Configuration::instance()->enableInMemory(); + } } } diff --git a/src/PHPUnit/EnableInMemoryBeforeTest.php b/src/PHPUnit/EnableInMemoryBeforeTest.php new file mode 100644 index 000000000..067b86274 --- /dev/null +++ b/src/PHPUnit/EnableInMemoryBeforeTest.php @@ -0,0 +1,44 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\PHPUnit; + +use PHPUnit\Event; +use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; +use Zenstruck\Foundry\Configuration; +use Zenstruck\Foundry\InMemory\AsInMemoryTest; +use Zenstruck\Foundry\InMemory\CannotEnableInMemory; + +final class EnableInMemoryBeforeTest implements Event\Test\PreparedSubscriber +{ + public function notify(Event\Test\Prepared $event): void + { + $test = $event->test(); + + if (!$test instanceof Event\Code\TestMethod) { + return; + } + + $testClass = $test->className(); + + if (!AsInMemoryTest::shouldEnableInMemory($testClass, $test->methodName())) { + return; + } + + if (!\is_subclass_of($testClass, KernelTestCase::class)) { + throw CannotEnableInMemory::testIsNotAKernelTestCase("{$test->className()}::{$test->methodName()}"); + } + + Configuration::instance()->enableInMemory(); + } +} diff --git a/src/PHPUnit/FoundryExtension.php b/src/PHPUnit/FoundryExtension.php index de88bb145..851042053 100644 --- a/src/PHPUnit/FoundryExtension.php +++ b/src/PHPUnit/FoundryExtension.php @@ -34,7 +34,7 @@ public function bootstrap( Configuration::shutdown(); } - $subscribers = [new BuildStoryOnTestPrepared()]; + $subscribers = [new BuildStoryOnTestPrepared(), new EnableInMemoryBeforeTest()]; if (ConstraintRequirement::from('>=11.4')->isSatisfiedBy(Runner\Version::id())) { // those deal with data provider events which can be useful only if PHPUnit >=11.4 is used diff --git a/src/Persistence/PersistentObjectFactory.php b/src/Persistence/PersistentObjectFactory.php index 3ce4e6064..f118bf4b7 100644 --- a/src/Persistence/PersistentObjectFactory.php +++ b/src/Persistence/PersistentObjectFactory.php @@ -276,7 +276,7 @@ public function persistMode(): PersistMode { $config = Configuration::instance(); - if (!$config->isPersistenceEnabled()) { + if (!$config->isPersistenceEnabled() || $config->isInMemoryEnabled()) { return PersistMode::WITHOUT_PERSISTING; } @@ -412,7 +412,7 @@ final protected function isPersisting(): bool { $config = Configuration::instance(); - if (!$config->isPersistenceEnabled()) { + if ($config->isInMemoryEnabled() || !$config->isPersistenceEnabled()) { return false; } diff --git a/src/ZenstruckFoundryBundle.php b/src/ZenstruckFoundryBundle.php index 3036897c4..db6ef4fb7 100644 --- a/src/ZenstruckFoundryBundle.php +++ b/src/ZenstruckFoundryBundle.php @@ -21,6 +21,8 @@ use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\HttpKernel\Bundle\AbstractBundle; use Zenstruck\Foundry\Attribute\AsFoundryHook; +use Zenstruck\Foundry\InMemory\DependencyInjection\InMemoryCompilerPass; +use Zenstruck\Foundry\InMemory\InMemoryRepository; use Zenstruck\Foundry\Mongo\MongoResetter; use Zenstruck\Foundry\Object\Event\Event; use Zenstruck\Foundry\Object\Event\HookListenerFilter; @@ -307,6 +309,10 @@ static function(ChildDefinition $definition, AsFoundryHook $attribute, \Reflecti ]); } ); + + $configurator->import('../config/in_memory.php'); + + $container->registerForAutoconfiguration(InMemoryRepository::class)->addTag('foundry.in_memory.repository'); } public function build(ContainerBuilder $container): void @@ -314,6 +320,7 @@ public function build(ContainerBuilder $container): void parent::build($container); $container->addCompilerPass($this); + $container->addCompilerPass(new InMemoryCompilerPass()); } public function process(ContainerBuilder $container): void diff --git a/tests/Fixture/InMemory/InMemoryAddressRepository.php b/tests/Fixture/InMemory/InMemoryAddressRepository.php new file mode 100644 index 000000000..940265cf1 --- /dev/null +++ b/tests/Fixture/InMemory/InMemoryAddressRepository.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Tests\Fixture\InMemory; + +use Zenstruck\Foundry\InMemory\InMemoryRepository; +use Zenstruck\Foundry\InMemory\InMemoryRepositoryTrait; +use Zenstruck\Foundry\Tests\Fixture\Entity\Address; + +/** + * @implements InMemoryRepository
+ */ +final class InMemoryAddressRepository implements InMemoryRepository +{ + /** @use InMemoryRepositoryTrait
*/ + use InMemoryRepositoryTrait; + + public static function _class(): string + { + return Address::class; + } +} diff --git a/tests/Fixture/InMemory/InMemoryContactRepository.php b/tests/Fixture/InMemory/InMemoryContactRepository.php new file mode 100644 index 000000000..f3014b5fb --- /dev/null +++ b/tests/Fixture/InMemory/InMemoryContactRepository.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Tests\Fixture\InMemory; + +use Zenstruck\Foundry\InMemory\InMemoryRepository; +use Zenstruck\Foundry\InMemory\InMemoryRepositoryTrait; +use Zenstruck\Foundry\Tests\Fixture\Entity\Contact; + +/** + * @implements InMemoryRepository + */ +final class InMemoryContactRepository implements InMemoryRepository +{ + /** @use InMemoryRepositoryTrait */ + use InMemoryRepositoryTrait; + + public static function _class(): string + { + return Contact::class; + } +} diff --git a/tests/Fixture/TestKernel.php b/tests/Fixture/TestKernel.php index c9e3be5ec..c99d69c31 100644 --- a/tests/Fixture/TestKernel.php +++ b/tests/Fixture/TestKernel.php @@ -18,6 +18,8 @@ use Zenstruck\Foundry\Tests\Fixture\Events\FoundryEventListener; use Zenstruck\Foundry\Tests\Fixture\Factories\ArrayFactory; use Zenstruck\Foundry\Tests\Fixture\Factories\Object1Factory; +use Zenstruck\Foundry\Tests\Fixture\InMemory\InMemoryAddressRepository; +use Zenstruck\Foundry\Tests\Fixture\InMemory\InMemoryContactRepository; use Zenstruck\Foundry\Tests\Fixture\Stories\ServiceStory; /** @@ -51,6 +53,8 @@ protected function configureContainer(ContainerBuilder $c, LoaderInterface $load $c->register(ArrayFactory::class)->setAutowired(true)->setAutoconfigured(true); $c->register(Object1Factory::class)->setAutowired(true)->setAutoconfigured(true); $c->register(ServiceStory::class)->setAutowired(true)->setAutoconfigured(true); + $c->register(InMemoryAddressRepository::class)->setAutowired(true)->setAutoconfigured(true); + $c->register(InMemoryContactRepository::class)->setAutowired(true)->setAutoconfigured(true); $c->register(FoundryEventListener::class)->setAutowired(true)->setAutoconfigured(true); } diff --git a/tests/Integration/DataProvider/DataProviderWithInMemoryTest.php b/tests/Integration/DataProvider/DataProviderWithInMemoryTest.php new file mode 100644 index 000000000..03e7149ba --- /dev/null +++ b/tests/Integration/DataProvider/DataProviderWithInMemoryTest.php @@ -0,0 +1,99 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Tests\Integration\DataProvider; + +use Doctrine\ORM\EntityManagerInterface; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\RequiresPhpunit; +use PHPUnit\Framework\Attributes\RequiresPhpunitExtension; +use PHPUnit\Framework\Attributes\Test; +use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; +use Zenstruck\Foundry\InMemory\AsInMemoryTest; +use Zenstruck\Foundry\Persistence\PersistentObjectFactory; +use Zenstruck\Foundry\PHPUnit\FoundryExtension; +use Zenstruck\Foundry\Test\Factories; +use Zenstruck\Foundry\Test\ResetDatabase; +use Zenstruck\Foundry\Tests\Fixture\Entity\Contact; +use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\Contact\ContactFactory; +use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\Contact\ProxyContactFactory; +use Zenstruck\Foundry\Tests\Fixture\InMemory\InMemoryContactRepository; +use Zenstruck\Foundry\Tests\Integration\RequiresORM; + +use function Zenstruck\Foundry\Persistence\unproxy; + +/** + * @author Nicolas PHILIPPE + * @requires PHPUnit 11.4 + */ +#[RequiresPhpunit('11.4')] +#[RequiresPhpunitExtension(FoundryExtension::class)] +final class DataProviderWithInMemoryTest extends KernelTestCase +{ + use Factories; + use RequiresORM; // needed to use the entity manager + use ResetDatabase; + + private InMemoryContactRepository $contactRepository; + + private EntityManagerInterface $entityManager; + + protected function setUp(): void + { + $this->contactRepository = self::getContainer()->get(InMemoryContactRepository::class); // @phpstan-ignore assign.propertyType + + $this->entityManager = self::getContainer()->get(EntityManagerInterface::class); // @phpstan-ignore assign.propertyType + } + + /** + * @param PersistentObjectFactory $factory + */ + #[Test] + #[DataProvider('provideContactFactory')] + #[AsInMemoryTest] + public function it_can_create_in_memory_factory_in_data_provider(PersistentObjectFactory $factory): void + { + if ('1' !== ($_ENV['USE_FOUNDRY_PHPUNIT_EXTENSION'] ?? null)) { + self::markTestSkipped('Needs Foundry PHPUnit extension.'); + } + + $contact = $factory->create(); + + self::assertSame([unproxy($contact)], $this->contactRepository->_all()); + + self::assertSame(0, $this->entityManager->getRepository(Contact::class)->count()); + } + + public static function provideContactFactory(): iterable + { + yield [ContactFactory::new()]; + yield [ProxyContactFactory::new()]; + } + + #[Test] + #[DataProvider('provideContact')] + #[AsInMemoryTest] + public function it_can_create_in_memory_objects_in_data_provider(?Contact $contact = null): void + { + self::assertInstanceOf(Contact::class, $contact); + + self::assertSame([unproxy($contact)], $this->contactRepository->_all()); + + self::assertSame(0, $this->entityManager->getRepository(Contact::class)->count()); + } + + public static function provideContact(): iterable + { + yield [ProxyContactFactory::createOne()]; + } +} diff --git a/tests/Integration/InMemory/InMemoryAttributeOnMethodTest.php b/tests/Integration/InMemory/InMemoryAttributeOnMethodTest.php new file mode 100644 index 000000000..9444f67c7 --- /dev/null +++ b/tests/Integration/InMemory/InMemoryAttributeOnMethodTest.php @@ -0,0 +1,77 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Tests\Integration\InMemory; + +use Doctrine\ORM\EntityManagerInterface; +use PHPUnit\Framework\Attributes\RequiresPhpunit; +use PHPUnit\Framework\Attributes\RequiresPhpunitExtension; +use PHPUnit\Framework\Attributes\Test; +use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; +use Zenstruck\Foundry\InMemory\AsInMemoryTest; +use Zenstruck\Foundry\PHPUnit\FoundryExtension; +use Zenstruck\Foundry\Test\Factories; +use Zenstruck\Foundry\Test\ResetDatabase; +use Zenstruck\Foundry\Tests\Fixture\Entity\Address; +use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\Address\AddressFactory; +use Zenstruck\Foundry\Tests\Integration\RequiresORM; + +/** + * @author Nicolas PHILIPPE + * @requires PHPUnit >=11.4 + */ +#[RequiresPhpunit('>=11.4')] +#[RequiresPhpunitExtension(FoundryExtension::class)] +final class InMemoryAttributeOnMethodTest extends KernelTestCase +{ + use Factories; + use RequiresORM; + use ResetDatabase; + + private EntityManagerInterface $entityManager; + + protected function setUp(): void + { + $this->entityManager = self::getContainer()->get(EntityManagerInterface::class); // @phpstan-ignore assign.propertyType + } + + /** + * @test + */ + #[Test] + #[AsInMemoryTest] + public function create_one_does_not_persist_in_database(): void + { + $address = AddressFactory::createOne(); + self::assertInstanceOf(Address::class, $address); + + self::assertSame(0, $this->entityManager->getRepository(Address::class)->count([])); + + // id is autogenerated from the db, then it should be null + self::assertNull($address->id); + } + + /** + * @test + */ + #[Test] + public function not_in_memory_test(): void + { + $address = AddressFactory::createOne(); + self::assertInstanceOf(Address::class, $address); + + self::assertSame(1, $this->entityManager->getRepository(Address::class)->count([])); + + self::assertNotNull($address->id); + } +} diff --git a/tests/Integration/InMemory/InMemoryTest.php b/tests/Integration/InMemory/InMemoryTest.php new file mode 100644 index 000000000..942b7d071 --- /dev/null +++ b/tests/Integration/InMemory/InMemoryTest.php @@ -0,0 +1,162 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Tests\Integration\InMemory; + +use Doctrine\ORM\EntityManagerInterface; +use PHPUnit\Framework\Attributes\RequiresPhpunit; +use PHPUnit\Framework\Attributes\RequiresPhpunitExtension; +use PHPUnit\Framework\Attributes\Test; +use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; +use Zenstruck\Foundry\InMemory\AsInMemoryTest; +use Zenstruck\Foundry\PHPUnit\FoundryExtension; +use Zenstruck\Foundry\Test\Factories; +use Zenstruck\Foundry\Test\ResetDatabase; +use Zenstruck\Foundry\Tests\Fixture\Entity\Address; +use Zenstruck\Foundry\Tests\Fixture\Entity\Category; +use Zenstruck\Foundry\Tests\Fixture\Entity\Contact; +use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\Address\AddressFactory; +use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\Category\CategoryFactory; +use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\Contact\ContactFactory; +use Zenstruck\Foundry\Tests\Fixture\InMemory\InMemoryAddressRepository; +use Zenstruck\Foundry\Tests\Fixture\InMemory\InMemoryContactRepository; +use Zenstruck\Foundry\Tests\Integration\RequiresORM; + +/** + * @author Nicolas PHILIPPE + * @requires PHPUnit >=11.4 + */ +#[RequiresPhpunit('>=11.4')] +#[RequiresPhpunitExtension(FoundryExtension::class)] +#[AsInMemoryTest] +final class InMemoryTest extends KernelTestCase +{ + use Factories; + use RequiresORM; + use ResetDatabase; + + private InMemoryAddressRepository $addressRepository; + private InMemoryContactRepository $contactRepository; + + private EntityManagerInterface $entityManager; + + protected function setUp(): void + { + $this->addressRepository = self::getContainer()->get(InMemoryAddressRepository::class); // @phpstan-ignore assign.propertyType + $this->contactRepository = self::getContainer()->get(InMemoryContactRepository::class); // @phpstan-ignore assign.propertyType + + $this->entityManager = self::getContainer()->get(EntityManagerInterface::class); // @phpstan-ignore assign.propertyType + } + + /** + * @test + */ + #[Test] + public function create_one_does_not_persist_in_database(): void + { + $address = AddressFactory::createOne(); + self::assertInstanceOf(Address::class, $address); + + self::assertSame(0, $this->entityManager->getRepository(Address::class)->count([])); + + // id is autogenerated from the db, then it should be null + self::assertNull($address->id); + } + + /** + * @test + */ + #[Test] + public function create_many_does_not_persist_in_database(): void + { + $addresses = AddressFactory::createMany(2); + self::assertContainsOnlyInstancesOf(Address::class, $addresses); + + self::assertSame(0, $this->entityManager->getRepository(Address::class)->count([])); + + foreach ($addresses as $address) { + // id is autogenerated from the db, then it should be null + self::assertNull($address->id); + } + } + + /** + * @test + */ + #[Test] + public function object_should_be_accessible_from_in_memory_repository(): void + { + $address = AddressFactory::createOne(); + + self::assertSame([$address], $this->addressRepository->_all()); + } + + /** + * @test + */ + #[Test] + public function nested_objects_should_be_accessible_from_their_respective_repository(): void + { + $contact = ContactFactory::createOne(); + + self::assertSame([$contact], $this->contactRepository->_all()); + self::assertSame([$contact->getAddress()], $this->addressRepository->_all()); + + self::assertSame(0, $this->entityManager->getRepository(Address::class)->count([])); + self::assertSame(0, $this->entityManager->getRepository(Contact::class)->count([])); + } + + /** + * @test + */ + #[Test] + public function can_use_generic_repository(): void + { + $category = CategoryFactory::createOne([ + 'contacts' => ContactFactory::new()->many(2), + ]); + + self::assertSame(0, $this->entityManager->getRepository(Category::class)->count([])); + + self::assertSame($this->contactRepository->_all(), $category->getContacts()->toArray()); + } + + /** + * @test + */ + #[Test] + public function one_to_many(): void + { + $category = CategoryFactory::createOne([ + 'contacts' => ContactFactory::new()->many(2), + ]); + + $this->assertCount(2, $category->getContacts()); + + foreach ($category->getContacts() as $contact) { + $this->assertSame($category, $contact->getCategory()); + } + } + + /** + * @test + */ + #[Test] + public function inversed_one_to_one(): void + { + $address = AddressFactory::createOne(['contact' => ContactFactory::new()]); + + self::assertNotNull($address->getContact()); + self::assertSame($address, $address->getContact()->getAddress()); + } +}