diff --git a/.gitignore b/.gitignore index 18ecc0e..69f6d2c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ vendor composer.lock .phpcs-cache +tests/_support/project/config/reference.php tests/_support/project/public/files/* !tests/_support/project/public/files/.gitkeep tests/_support/project/var/cache/* diff --git a/composer.json b/composer.json index 8c42e1e..1367f9c 100644 --- a/composer.json +++ b/composer.json @@ -30,7 +30,7 @@ "codeception/module-doctrine": "^3.1", "codeception/module-symfony": "*", "doctrine/dbal": "^3.4|^4.0", - "doctrine/doctrine-bundle": "^2.4", + "doctrine/doctrine-bundle": "^2.4|^3.0", "doctrine/lexer": "^1.2|^2.0|^3.0", "doctrine/orm": "^2.13|^3.0", "doctrine/persistence": "^2.0|^3.0|^4.0", @@ -44,7 +44,7 @@ "phpstan/phpstan": "^2.1.46", "phpstan/phpstan-beberlei-assert": "^2.0", "phpstan/phpstan-phpunit": "^2.0", - "phpunit/phpunit": "^9.5", + "phpunit/phpunit": "^10.0", "psr/http-client": "^1.0", "squizlabs/php_codesniffer": "^4.0", "symfony/asset": "^4.4.30|^5.4|^6.0|^7.0", @@ -69,6 +69,7 @@ "twig/twig": "^3.7" }, "conflict": { + "doctrine/collections": "<1.7", "doctrine/persistence": "<2.0", "symfony/property-info": ">=8.0" }, diff --git a/phpstan.neon b/phpstan.neon index 42ad403..1a88940 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -9,6 +9,7 @@ parameters: - src - tests inferPrivatePropertyTypeFromConstructor: true + reportUnmatchedIgnoredErrors: false excludePaths: - 'tests/_support/_generated' - 'tests/_support/project/var' @@ -18,36 +19,30 @@ parameters: - message: '#^Parameter \#1 \$errors of method Tests\\FSi\\App\\Controller\\[a-zA-z]{1,}Controller::formErrorsToMessage\(\).{1,}$#' path: 'tests/_support/project/src/Controller' - reportUnmatched: false - message: '#^PHPDoc tag \@param for parameter \$event contains generic type Doctrine\\Persistence\\Event\\LifecycleEventArgs but class Doctrine\\Persistence\\Event\\LifecycleEventArgs is not generic\.$#' path: 'src/Integration/Doctrine/ORM/Subscriber/EntitySubscriber.php' - reportUnmatched: false - message: '#^Call to an undefined method Doctrine\\ORM\\Event\\\w+::getEntityManager\(\)\.$#' path: 'src/Integration/Doctrine/ORM/Subscriber/EntitySubscriber.php' - reportUnmatched: false - message: '#^Call to function method_exists\(\) with Doctrine\\ORM\\Event\\\w+ and ''getObjectManager'' will always evaluate to true\.$#' path: 'src/Integration/Doctrine/ORM/Subscriber/EntitySubscriber.php' - reportUnmatched: false + - + message: '#^Call to an undefined method ReflectionClass\::\w+\(\)\.$#' + path: 'src/Integration/Doctrine/ORM/Subscriber/EntitySubscriber.php' - message: '#.*Symfony\\Component\\Routing\\RouteCollectionBuilder.*#' path: '%currentWorkingDirectory%/tests/_support/project/src/Kernel.php' - reportUnmatched: false - message: '#Doctrine\\ORM\\Mapping\\EmbeddedClassMapping( not found)?#' path: '%currentWorkingDirectory%/src/Integration/Doctrine/ORM/Entity/EmptyEmbeddableVerifier.php' - reportUnmatched: false - identifier: 'trait.unused' path: '%currentWorkingDirectory%/src/Integration/Doctrine/ORM/ProxyTrait.php' - reportUnmatched: false - identifier: 'property.unusedType' path: 'tests/_support/project/src/Entity' - reportUnmatched: false - identifier: 'arrayValues.list' path: 'tests/_support/Module/DbModule.php' - reportUnmatched: false diff --git a/src/Integration/Doctrine/ORM/Subscriber/EntitySubscriber.php b/src/Integration/Doctrine/ORM/Subscriber/EntitySubscriber.php index ce21fc4..3ecc8b5 100644 --- a/src/Integration/Doctrine/ORM/Subscriber/EntitySubscriber.php +++ b/src/Integration/Doctrine/ORM/Subscriber/EntitySubscriber.php @@ -23,12 +23,15 @@ use FSi\Component\Translatable\Entity\TranslationLoader; use FSi\Component\Translatable\Entity\TranslationUpdater; use FSi\Component\Translatable\LocaleProvider; +use ReflectionClass; use function array_walk; use function get_class; use function in_array; use function method_exists; +use const PHP_VERSION_ID; + final class EntitySubscriber { private LocaleProvider $localeProvider; @@ -233,6 +236,11 @@ private function initializeProxy(object $object): void { if (true === $object instanceof Proxy && false === $object->__isInitialized()) { $object->__load(); + } elseif (PHP_VERSION_ID >= 80400) { + $reflector = new ReflectionClass($object); + if (true === $reflector->isUninitializedLazyObject($object)) { + $reflector->initializeLazyObject($object); + } } } } diff --git a/tests/_support/project/config/packages/doctrine.yaml b/tests/_support/project/config/packages/doctrine.yaml index 5cb3468..10318e6 100644 --- a/tests/_support/project/config/packages/doctrine.yaml +++ b/tests/_support/project/config/packages/doctrine.yaml @@ -6,7 +6,6 @@ doctrine: charset: UTF8 path: '%kernel.project_dir%/var/data.sqlite' orm: - auto_generate_proxy_classes: true naming_strategy: doctrine.orm.naming_strategy.underscore auto_mapping: true mappings: diff --git a/tests/_support/project/src/Entity/Article.php b/tests/_support/project/src/Entity/Article.php index 7848c3d..a4c1ef9 100644 --- a/tests/_support/project/src/Entity/Article.php +++ b/tests/_support/project/src/Entity/Article.php @@ -31,12 +31,17 @@ class Article * @var Collection */ private Collection $comments; + /** + * @var Collection + */ + private Collection $posts; public function __construct(?string $title = null, ?string $description = null) { $this->title = $title; $this->description = $description; $this->comments = new ArrayCollection(); + $this->posts = new ArrayCollection(); } public function getId(): ?int @@ -141,4 +146,12 @@ public function removeComment(Comment $comment): void { $this->comments->removeElement($comment); } + + /** + * @return list + */ + public function getPosts(): array + { + return $this->posts->getValues(); + } } diff --git a/tests/_support/project/src/Entity/Post.php b/tests/_support/project/src/Entity/Post.php index 820488f..425a8a3 100644 --- a/tests/_support/project/src/Entity/Post.php +++ b/tests/_support/project/src/Entity/Post.php @@ -17,6 +17,7 @@ class Post private ?string $locale; private ?string $title; private ?string $content; + private ?Article $article = null; public function __construct(?string $title = null, ?string $content = null) { @@ -58,4 +59,9 @@ public function setContent(?string $content): void { $this->content = $content; } + + public function getArticle(): ?Article + { + return $this->article; + } } diff --git a/tests/_support/project/src/Resources/config/doctrine/Article.orm.xml b/tests/_support/project/src/Resources/config/doctrine/Article.orm.xml index 63ed301..48400b7 100644 --- a/tests/_support/project/src/Resources/config/doctrine/Article.orm.xml +++ b/tests/_support/project/src/Resources/config/doctrine/Article.orm.xml @@ -12,6 +12,8 @@ + + diff --git a/tests/_support/project/src/Resources/config/doctrine/Post.orm.xml b/tests/_support/project/src/Resources/config/doctrine/Post.orm.xml index 9a7a810..0390515 100644 --- a/tests/_support/project/src/Resources/config/doctrine/Post.orm.xml +++ b/tests/_support/project/src/Resources/config/doctrine/Post.orm.xml @@ -10,6 +10,8 @@ + + diff --git a/tests/functional/Entity/ProxyLoadingCest.php b/tests/functional/Entity/ProxyLoadingCest.php new file mode 100644 index 0000000..f8ed640 --- /dev/null +++ b/tests/functional/Entity/ProxyLoadingCest.php @@ -0,0 +1,44 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Tests\FSi\Component\Translatable\Entity; + +use DateTimeImmutable; +use Tests\FSi\App\Entity\Article; +use Tests\FSi\App\Entity\Post; +use Tests\FSi\FunctionalTester; + +final class ProxyLoadingCest +{ + public function testProxyLoading(FunctionalTester $I): void + { + $articleId = $I->haveInRepository(Article::class, [ + 'locale' => 'en', + 'publicationDate' => new DateTimeImmutable(), + 'title' => 'Title', + 'description' => 'Description', + ]); + $article = $I->grabEntityFromRepository(Article::class, ['id' => $articleId]); + + $postId = $I->haveInRepository(Post::class, [ + 'locale' => 'en', + 'title' => 'Title', + 'content' => 'Content', + 'article' => $article, + ]); + + $I->clearEntityManager(); + + $post = $I->grabEntityFromRepository(Post::class, ['id' => $postId]); + $I->assertInstanceOf(Article::class, $post->getArticle()); + $I->flushToDatabase(); + } +}