diff --git a/Build/phpstan/phpstan-baseline.neon b/Build/phpstan/phpstan-baseline.neon index 63ed37a9fc76e2196dc5bb8f4586cb30affb9385..b9c1b98485c5d2102437072148588605485bdb5c 100644 --- a/Build/phpstan/phpstan-baseline.neon +++ b/Build/phpstan/phpstan-baseline.neon @@ -1970,26 +1970,6 @@ parameters: count: 6 path: ../../typo3/sysext/extbase/Tests/Unit/Persistence/Generic/SessionTest.php - - - message: "#^Call to an undefined method PHPUnit\\\\Framework\\\\MockObject\\\\MockObject&TYPO3\\\\CMS\\\\Extbase\\\\Persistence\\\\Repository&TYPO3\\\\TestingFramework\\\\Core\\\\AccessibleObjectInterface\\:\\:findOneByFoo\\(\\)\\.$#" - count: 2 - path: ../../typo3/sysext/extbase/Tests/Unit/Persistence/RepositoryTest.php - - - - message: "#^Call to an undefined method PHPUnit\\\\Framework\\\\MockObject\\\\MockObject&TYPO3\\\\CMS\\\\Extbase\\\\Persistence\\\\Repository\\:\\:countByFoo\\(\\)\\.$#" - count: 1 - path: ../../typo3/sysext/extbase/Tests/Unit/Persistence/RepositoryTest.php - - - - message: "#^Call to an undefined method PHPUnit\\\\Framework\\\\MockObject\\\\MockObject&TYPO3\\\\CMS\\\\Extbase\\\\Persistence\\\\Repository\\:\\:findByFoo\\(\\)\\.$#" - count: 1 - path: ../../typo3/sysext/extbase/Tests/Unit/Persistence/RepositoryTest.php - - - - message: "#^Call to an undefined method PHPUnit\\\\Framework\\\\MockObject\\\\MockObject&TYPO3\\\\CMS\\\\Extbase\\\\Persistence\\\\Repository\\:\\:findOneByFoo\\(\\)\\.$#" - count: 1 - path: ../../typo3/sysext/extbase/Tests/Unit/Persistence/RepositoryTest.php - - message: "#^Call to an undefined method TYPO3\\\\CMS\\\\Extbase\\\\Persistence\\\\Generic\\\\Session\\:\\:method\\(\\)\\.$#" count: 1 @@ -1997,7 +1977,7 @@ parameters: - message: "#^Call to an undefined method TYPO3\\\\CMS\\\\Extbase\\\\Persistence\\\\QueryInterface\\:\\:expects\\(\\)\\.$#" - count: 10 + count: 2 path: ../../typo3/sysext/extbase/Tests/Unit/Persistence/RepositoryTest.php - @@ -2120,6 +2100,31 @@ parameters: count: 5 path: ../../typo3/sysext/extbase/Tests/Unit/Utility/ExtensionUtilityTest.php + - + message: "#^Call to an undefined method PHPUnit\\\\Framework\\\\MockObject\\\\MockObject&TYPO3\\\\CMS\\\\Extbase\\\\Persistence\\\\Repository&TYPO3\\\\TestingFramework\\\\Core\\\\AccessibleObjectInterface\\:\\:findOneByFoo\\(\\)\\.$#" + count: 2 + path: ../../typo3/sysext/extbase/Tests/UnitDeprecated/Persistence/RepositoryTest.php + + - + message: "#^Call to an undefined method PHPUnit\\\\Framework\\\\MockObject\\\\MockObject&TYPO3\\\\CMS\\\\Extbase\\\\Persistence\\\\Repository\\:\\:countByFoo\\(\\)\\.$#" + count: 1 + path: ../../typo3/sysext/extbase/Tests/UnitDeprecated/Persistence/RepositoryTest.php + + - + message: "#^Call to an undefined method PHPUnit\\\\Framework\\\\MockObject\\\\MockObject&TYPO3\\\\CMS\\\\Extbase\\\\Persistence\\\\Repository\\:\\:findByFoo\\(\\)\\.$#" + count: 1 + path: ../../typo3/sysext/extbase/Tests/UnitDeprecated/Persistence/RepositoryTest.php + + - + message: "#^Call to an undefined method PHPUnit\\\\Framework\\\\MockObject\\\\MockObject&TYPO3\\\\CMS\\\\Extbase\\\\Persistence\\\\Repository\\:\\:findOneByFoo\\(\\)\\.$#" + count: 1 + path: ../../typo3/sysext/extbase/Tests/UnitDeprecated/Persistence/RepositoryTest.php + + - + message: "#^Call to an undefined method TYPO3\\\\CMS\\\\Extbase\\\\Persistence\\\\QueryInterface\\:\\:expects\\(\\)\\.$#" + count: 8 + path: ../../typo3/sysext/extbase/Tests/UnitDeprecated/Persistence/RepositoryTest.php + - message: "#^Parameter \\#2 \\$messageTitle of method TYPO3\\\\CMS\\\\Extbase\\\\Mvc\\\\Controller\\\\ActionController\\:\\:addFlashMessage\\(\\) expects string, int given\\.$#" count: 1 @@ -2150,11 +2155,6 @@ parameters: count: 1 path: ../../typo3/sysext/extensionmanager/Classes/Service/ExtensionManagementService.php - - - message: "#^Call to an undefined method TYPO3\\\\CMS\\\\Extensionmanager\\\\Domain\\\\Repository\\\\ExtensionRepository\\:\\:countByExtensionKey\\(\\)\\.$#" - count: 1 - path: ../../typo3/sysext/extensionmanager/Classes/Utility/DependencyUtility.php - - message: "#^Offset string does not exist on null\\.$#" count: 1 diff --git a/typo3/sysext/core/Documentation/Changelog/12.3/Deprecation-100071-MagicRepositoryFindByMethods.rst b/typo3/sysext/core/Documentation/Changelog/12.3/Deprecation-100071-MagicRepositoryFindByMethods.rst new file mode 100644 index 0000000000000000000000000000000000000000..c74b8db8ff366af85043f9bbd2764e3570bcbfde --- /dev/null +++ b/typo3/sysext/core/Documentation/Changelog/12.3/Deprecation-100071-MagicRepositoryFindByMethods.rst @@ -0,0 +1,82 @@ +.. include:: /Includes.rst.txt + +.. _deprecation-100071-1677853787: + +======================================================== +Deprecation: #100071 - Magic repository findBy() methods +======================================================== + +See :issue:`100071` + +Description +=========== + +Extbase repositories come with a magic :php:`__call()`-method to allow calling +the following methods without implementing: + +- :php:`findBy[PropertyName]($propertyValue)` +- :php:`findOneBy[PropertyName]($propertyValue)` +- :php:`countBy[PropertyName]($propertyValue)` + +These have now been marked as deprecated, as they are "magic", meaning +that proper IDE support is not possible, and other PHP-related tooling +functionality such as PHPStorm. + +In addition, with the magic methods, it is not possible for Extbase repositories +to build their own magic method functionality, as the logic is already +in use. + +Impact +====== + +As these methods are widely used in almost all Extbase-based extensions, +they are marked as deprecated in TYPO3 v12, but will only trigger a deprecation +notice in TYPO3 v13, as they will be removed in TYPO3 v14. + +This way, the migration towards the new API methods can be taken without +pressure. + + +Affected installations +====================== + +All installations with third-party extensions that use those magic methods. + + +Migration +========= + +A new set of methods without all those downsides have been added: + +- :php:`findBy(array $criteria, ...): QueryResultInterface` +- :php:`findOneBy(array $criteria, ...):object|null` +- :php:`count(array $criteria, ...): int` + +The naming of those methods follows those of `doctrine/orm` and only +:php:`count()` differs from the formerly :php:`countBy()`. While all magic +methods only allow for a single comparison (`propertyName` = `propertyValue`), +those methods allow for multiple comparisons, called constraints. + + +`findBy[PropertyName]($propertyValue)` can be replaced with a call to `findBy`: + +.. code-block:: php + + $this->blogRepository->findBy(['propertyName' => $propertyValue]); + + +`findOneBy[PropertyName]($propertyValue)` can be replaced with a call to `findOneBy`: + +.. code-block:: php + + $this->blogRepository->findOneBy(['propertyName' => $propertyValue]); + + +`countBy[PropertyName]($propertyValue)` can be replaced with a call to `count`: + +.. code-block:: php + + $this->blogRepository->count(['propertyName' => $propertyValue]); + + +.. index:: PHP-API, NotScanned, ext:extbase diff --git a/typo3/sysext/core/Documentation/Changelog/12.3/Feature-100071-IntroduceNon-magicRepositoryFindMethods.rst b/typo3/sysext/core/Documentation/Changelog/12.3/Feature-100071-IntroduceNon-magicRepositoryFindMethods.rst new file mode 100644 index 0000000000000000000000000000000000000000..c0f0e17cf316b376160afa38f14dc1666dc7978d --- /dev/null +++ b/typo3/sysext/core/Documentation/Changelog/12.3/Feature-100071-IntroduceNon-magicRepositoryFindMethods.rst @@ -0,0 +1,51 @@ +.. include:: /Includes.rst.txt + +.. _feature-100071-1677853567: + +============================================================== +Feature: #100071 - Introduce non-magic repository find methods +============================================================== + +See :issue:`100071` + +Description +=========== + +Extbase repositories come with a magic :php:`__call()`-method to allow calling +the following methods without implementing: + +- :php:`findBy[PropertyName]($propertyValue)` +- :php:`findOneBy[PropertyName]($propertyValue)` +- :php:`countBy[PropertyName]($propertyValue)` + +Magic methods are quite handy but they have a huge disadvantage. There is no +proper IDE support i.e. most IDEs show an error or at least a warning, +saying method :php:`findByAuthor()` does not exist. Also, type declarations are +impossible to use because with :php:`__call()` everything is :php:`mixed`. And +last but not least, static code analysis - like phpstan - cannot properly +analyse those and give meaningful errors. + +Therefore, there is a new set of methods without all those downsides: + +- :php:`findBy(array $criteria, ...): QueryResultInterface` +- :php:`findOneBy(array $criteria, ...):object|null` +- :php:`count(array $criteria, ...): int` + +The naming of those methods follows those of `doctrine/orm` and only +:php:`count()` differs from the formerly :php:`countBy()`. While all magic +methods only allow for a single comparison (`propertyName` = `propertyValue`), +those methods allow for multiple comparisons, called constraints. + +Example: + +.. code-block:: php + + $this->blogRepository->findBy(['author' => 1, 'published' => true]); + +Impact +====== + +Those new methods support a broader feature set, support IDEs, static code +analysers and type declarations. + +.. index:: PHP-API, NotScanned, ext:extbase diff --git a/typo3/sysext/extbase/Classes/Persistence/Repository.php b/typo3/sysext/extbase/Classes/Persistence/Repository.php index 507da27aeeb183759fbd246077a88e12489b1742..72eae9a267031a937473e590affa00e4f210bad7 100644 --- a/typo3/sysext/extbase/Classes/Persistence/Repository.php +++ b/typo3/sysext/extbase/Classes/Persistence/Repository.php @@ -223,16 +223,27 @@ class Repository implements RepositoryInterface, SingletonInterface * @param array<int, mixed> $arguments The arguments of the magic method * @throws UnsupportedMethodException * @return mixed + * @deprecated since v12, will be removed in v14, use {@see findBy}, {@see findOneBy} and {@see count} instead */ public function __call($methodName, $arguments) { if (str_starts_with($methodName, 'findBy') && strlen($methodName) > 7) { + // @todo Enable in version 13.0 + // trigger_error( + // 'Usage of magic method ' . static::class . '->findBy[Property]() is deprecated, use method findBy() instead.', + // E_USER_DEPRECATED + // ); $propertyName = lcfirst(substr($methodName, 6)); $query = $this->createQuery(); $result = $query->matching($query->equals($propertyName, $arguments[0]))->execute(); return $result; } if (str_starts_with($methodName, 'findOneBy') && strlen($methodName) > 10) { + // @todo Enable in version 13.0 + // trigger_error( + // 'Usage of magic method ' . static::class . '->findOneBy[Property]() is deprecated, use method findOneBy() instead.', + // E_USER_DEPRECATED + // ); $propertyName = lcfirst(substr($methodName, 9)); $query = $this->createQuery(); @@ -244,6 +255,11 @@ class Repository implements RepositoryInterface, SingletonInterface return $result[0] ?? null; } } elseif (str_starts_with($methodName, 'countBy') && strlen($methodName) > 8) { + // @todo Enable in version 13.0 + // trigger_error( + // 'Usage of magic method ' . static::class . '->countBy[Property]() is deprecated, use method count() instead.', + // E_USER_DEPRECATED + // ); $propertyName = lcfirst(substr($methodName, 7)); $query = $this->createQuery(); $result = $query->matching($query->equals($propertyName, $arguments[0]))->execute()->count(); @@ -252,6 +268,62 @@ class Repository implements RepositoryInterface, SingletonInterface throw new UnsupportedMethodException('The method "' . $methodName . '" is not supported by the repository.', 1233180480); } + /** + * @phpstan-param array<non-empty-string, mixed> $criteria + * @phpstan-param array<non-empty-string, QueryInterface::ORDER_*>|null $orderBy + * @phpstan-param 0|positive-int|null $limit + * @phpstan-param 0|positive-int|null $offset + * @phpstan-return QueryResultInterface<T> + * @return QueryResultInterface + */ + public function findBy(array $criteria, array $orderBy = null, int $limit = null, int $offset = null): QueryResultInterface + { + $query = $this->createQuery(); + $constraints = []; + foreach ($criteria as $propertyName => $propertyValue) { + $constraints[] = $query->equals($propertyName, $propertyValue); + } + + if (($numberOfConstraints = count($constraints)) === 1) { + $query->matching(...$constraints); + } elseif ($numberOfConstraints > 1) { + $query->matching($query->logicalAnd(...$constraints)); + } + + if (is_array($orderBy)) { + $query->setOrderings($orderBy); + } + + if (is_int($limit)) { + $query->setLimit($limit); + } + + if (is_int($offset)) { + $query->setOffset($offset); + } + + return $query->execute(); + } + + /** + * @phpstan-param array<non-empty-string, mixed> $criteria + * @phpstan-param array<non-empty-string, QueryInterface::ORDER_*>|null $orderBy + * @phpstan-return T|null + */ + public function findOneBy(array $criteria, array $orderBy = null): object|null + { + return $this->findBy($criteria, $orderBy, 1)->getFirst(); + } + + /** + * @phpstan-param array<non-empty-string, mixed> $criteria + * @phpstan-return 0|positive-int + */ + public function count(array $criteria): int + { + return $this->findBy($criteria)->count(); + } + /** * Returns the class name of this class. * diff --git a/typo3/sysext/extbase/Tests/Functional/Persistence/RepositoryTest.php b/typo3/sysext/extbase/Tests/Functional/Persistence/RepositoryTest.php new file mode 100644 index 0000000000000000000000000000000000000000..4d7814481849ec02342e2110f51bae3670db20ff --- /dev/null +++ b/typo3/sysext/extbase/Tests/Functional/Persistence/RepositoryTest.php @@ -0,0 +1,308 @@ +<?php + +declare(strict_types=1); + +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ + +namespace TYPO3\CMS\Extbase\Tests\Functional\Persistence; + +use ExtbaseTeam\BlogExample\Domain\Model\Post; +use ExtbaseTeam\BlogExample\Domain\Repository\PostRepository; +use TYPO3\CMS\Extbase\Persistence\QueryInterface; +use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase; + +class RepositoryTest extends FunctionalTestCase +{ + protected array $testExtensionsToLoad = ['typo3/sysext/extbase/Tests/Functional/Fixtures/Extensions/blog_example']; + + protected PostRepository $postRepository; + + protected function setUp(): void + { + parent::setUp(); + + $this->importCSVDataSet(__DIR__ . '/../Fixtures/pages.csv'); + $this->importCSVDataSet(__DIR__ . '/../Persistence/Fixtures/blogs.csv'); + $this->importCSVDataSet(__DIR__ . '/../Persistence/Fixtures/posts.csv'); + $this->importCSVDataSet(__DIR__ . '/../Persistence/Fixtures/tags.csv'); + $this->importCSVDataSet(__DIR__ . '/../Persistence/Fixtures/post-tag-mm.csv'); + $this->importCSVDataSet(__DIR__ . '/../Persistence/Fixtures/persons.csv'); + + $this->postRepository = $this->get(PostRepository::class); + } + + public function findByRespectsSingleCriteriaDataProvider(): \Generator + { + yield 'findBy(["blog" => 1]) => 10' => [ + ['blog' => 1], + 10, + ]; + + yield 'findBy(["blog" => 1]) => 1' => [ + ['blog' => 2], + 1, + ]; + + yield 'findBy(["blog" => 1]) => 3' => [ + ['blog' => 3], + 3, + ]; + } + + /** + * @test + * @dataProvider findByRespectsSingleCriteriaDataProvider + */ + public function findByRespectsSingleCriteria(array $criteria, int $expectedCount): void + { + self::assertCount($expectedCount, $this->postRepository->findBy($criteria)); + } + + /** + * @test + */ + public function findByRespectsMultipleCriteria(): void + { + self::assertCount(6, $this->postRepository->findBy(['blog' => 1, 'author' => 1])); + } + + /** + * @test + */ + public function findByRespectsSingleOrderBy(): void + { + $posts = $this->postRepository->findBy( + ['blog' => 1, 'author' => 1], + ['title' => QueryInterface::ORDER_DESCENDING] + )->toArray(); + + $titles = array_map(fn (Post $post) => $post->getTitle(), $posts); + + self::assertSame([ + 'Post9', + 'Post8', + 'Post7', + 'Post5', + 'Post4', + 'Post10', + ], $titles); + } + + /** + * @test + */ + public function findByRespectsMultipleOrderBy(): void + { + $posts = $this->postRepository->findBy( + [], + ['blog.uid' => QueryInterface::ORDER_ASCENDING, 'title' => QueryInterface::ORDER_DESCENDING] + )->toArray(); + + self::assertSame( + [ + [ + 'blog.uid' => 1, + 'post.title' => 'Post9', + ], + [ + 'blog.uid' => 1, + 'post.title' => 'Post8', + ], + [ + 'blog.uid' => 1, + 'post.title' => 'Post7', + ], + [ + 'blog.uid' => 1, + 'post.title' => 'Post6', + ], + [ + 'blog.uid' => 1, + 'post.title' => 'Post5', + ], + [ + 'blog.uid' => 1, + 'post.title' => 'Post4', + ], + [ + 'blog.uid' => 1, + 'post.title' => 'Post3', + ], + [ + 'blog.uid' => 1, + 'post.title' => 'Post2', + ], + [ + 'blog.uid' => 1, + 'post.title' => 'Post10', + ], + [ + 'blog.uid' => 1, + 'post.title' => 'Post1', + ], + [ + 'blog.uid' => 2, + 'post.title' => 'post1', + ], + [ + 'blog.uid' => 3, + 'post.title' => 'post with tagged author', + ], + [ + 'blog.uid' => 3, + 'post.title' => 'post with tag and tagged author', + ], + [ + 'blog.uid' => 3, + 'post.title' => 'post with tag', + ], + ], + array_map(fn (Post $post) => ['blog.uid' => $post->getBlog()->getUid(), 'post.title' => $post->getTitle()], $posts) + ); + } + + /** + * @test + */ + public function findByRespectsLimit(): void + { + $posts = $this->postRepository->findBy( + ['author' => 1], + ['uid' => QueryInterface::ORDER_DESCENDING], + 3 + )->toArray(); + + $titles = array_map(fn (Post $post) => ['uid' => $post->getUid(), 'title' => $post->getTitle()], $posts); + + self::assertSame([ + [ + 'uid' => 14, + 'title' => 'post with tag and tagged author', + ], + [ + 'uid' => 13, + 'title' => 'post with tagged author', + ], + [ + 'uid' => 10, + 'title' => 'Post10', + ], + ], $titles); + } + + /** + * @test + */ + public function findByRespectsOffset(): void + { + $posts = $this->postRepository->findBy( + ['author' => 1], + ['uid' => QueryInterface::ORDER_DESCENDING], + 3, + 1 + )->toArray(); + + $titles = array_map(fn (Post $post) => ['uid' => $post->getUid(), 'title' => $post->getTitle()], $posts); + + self::assertSame([ + [ + 'uid' => 13, + 'title' => 'post with tagged author', + ], + [ + 'uid' => 10, + 'title' => 'Post10', + ], + [ + 'uid' => 9, + 'title' => 'Post9', + ], + ], $titles); + } + + public function findOneByRespectsSingleCriteriaDataProvider(): \Generator + { + yield 'findOneBy(["blog" => 1]) => "Post4"' => [ + ['uid' => 1], + 1, + ]; + + yield 'findOneBy(["blog" => 100]) => null' => [ + ['uid' => 100], + null, + ]; + } + + /** + * @test + * @dataProvider findOneByRespectsSingleCriteriaDataProvider + */ + public function findOneByRespectsSingleCriteria(array $criteria, int|null $expectedUid): void + { + /** @var Post|null $post */ + $post = $this->postRepository->findOneBy($criteria); + + self::assertSame($expectedUid, $post?->getUid()); + } + + /** + * @test + * @group not-postgres + */ + public function findOneByRespectsMultipleCriteria(): void + { + $post = $this->postRepository->findOneBy(['blog' => 1, 'author' => 1]); + + self::assertSame('Post4', $post?->getTitle()); + } + + /** + * @test + */ + public function findOneByRespectsOrderBy(): void + { + $post = $this->postRepository->findOneBy( + ['blog' => 1, 'author' => 1], + ['title' => QueryInterface::ORDER_DESCENDING] + ); + + self::assertSame('Post9', $post?->getTitle()); + } + + /** + * @test + */ + public function countRespectsSingleCriteria(): void + { + self::assertSame( + 10, + $this->postRepository->count( + ['blog' => 1], + ) + ); + } + + /** + * @test + */ + public function countRespectsMultipleCriteria(): void + { + self::assertSame( + 1, + $this->postRepository->count( + ['blog' => 1, 'author' => 3], + ) + ); + } +} diff --git a/typo3/sysext/extbase/Tests/Unit/Persistence/RepositoryTest.php b/typo3/sysext/extbase/Tests/Unit/Persistence/RepositoryTest.php index 17a74bb84af9ac61e10a7935a5d540893be91058..dfec862b45d5730e6b6f8dba1b7008694091c6d5 100644 --- a/typo3/sysext/extbase/Tests/Unit/Persistence/RepositoryTest.php +++ b/typo3/sysext/extbase/Tests/Unit/Persistence/RepositoryTest.php @@ -226,67 +226,6 @@ class RepositoryTest extends UnitTestCase $this->repository->update($object); } - /** - * @test - */ - public function magicCallMethodAcceptsFindBySomethingCallsAndExecutesAQueryWithThatCriteria(): void - { - $mockQueryResult = $this->createMock(QueryResultInterface::class); - $mockQuery = $this->createMock(QueryInterface::class); - $mockQuery->expects(self::once())->method('equals')->with('foo', 'bar')->willReturn('matchCriteria'); - $mockQuery->expects(self::once())->method('matching')->with('matchCriteria')->willReturn($mockQuery); - $mockQuery->expects(self::once())->method('execute')->with()->willReturn($mockQueryResult); - - $repository = $this->getMockBuilder(Repository::class) - ->onlyMethods(['createQuery']) - ->getMock(); - $repository->expects(self::once())->method('createQuery')->willReturn($mockQuery); - - self::assertSame($mockQueryResult, $repository->findByFoo('bar')); - } - - /** - * @test - */ - public function magicCallMethodAcceptsFindOneBySomethingCallsAndExecutesAQueryWithThatCriteria(): void - { - $object = new \stdClass(); - $mockQueryResult = $this->createMock(QueryResultInterface::class); - $mockQueryResult->expects(self::once())->method('getFirst')->willReturn($object); - $mockQuery = $this->createMock(QueryInterface::class); - $mockQuery->expects(self::once())->method('equals')->with('foo', 'bar')->willReturn('matchCriteria'); - $mockQuery->expects(self::once())->method('matching')->with('matchCriteria')->willReturn($mockQuery); - $mockQuery->expects(self::once())->method('setLimit')->willReturn($mockQuery); - $mockQuery->expects(self::once())->method('execute')->willReturn($mockQueryResult); - - $repository = $this->getMockBuilder(Repository::class) - ->onlyMethods(['createQuery']) - ->getMock(); - $repository->expects(self::once())->method('createQuery')->willReturn($mockQuery); - - self::assertSame($object, $repository->findOneByFoo('bar')); - } - - /** - * @test - */ - public function magicCallMethodAcceptsCountBySomethingCallsAndExecutesAQueryWithThatCriteria(): void - { - $mockQuery = $this->createMock(QueryInterface::class); - $mockQueryResult = $this->createMock(QueryResultInterface::class); - $mockQuery->expects(self::once())->method('equals')->with('foo', 'bar')->willReturn('matchCriteria'); - $mockQuery->expects(self::once())->method('matching')->with('matchCriteria')->willReturn($mockQuery); - $mockQuery->expects(self::once())->method('execute')->willReturn($mockQueryResult); - $mockQueryResult->expects(self::once())->method('count')->willReturn(2); - - $repository = $this->getMockBuilder(Repository::class) - ->onlyMethods(['createQuery']) - ->getMock(); - $repository->expects(self::once())->method('createQuery')->willReturn($mockQuery); - - self::assertSame(2, $repository->countByFoo('bar')); - } - /** * @test */ @@ -389,34 +328,4 @@ class RepositoryTest extends UnitTestCase $this->repository->_set('objectType', 'Foo'); $this->repository->update(new \stdClass()); } - - /** - * @test - */ - public function magicCallMethodReturnsFirstArrayKeyInFindOneBySomethingIfQueryReturnsRawResult(): void - { - $queryResultArray = [ - 0 => [ - 'foo' => 'bar', - ], - ]; - $this->mockQuery->expects(self::once())->method('equals')->with('foo', 'bar')->willReturn('matchCriteria'); - $this->mockQuery->expects(self::once())->method('matching')->with('matchCriteria')->willReturn($this->mockQuery); - $this->mockQuery->expects(self::once())->method('setLimit')->with(1)->willReturn($this->mockQuery); - $this->mockQuery->expects(self::once())->method('execute')->willReturn($queryResultArray); - self::assertSame(['foo' => 'bar'], $this->repository->findOneByFoo('bar')); - } - - /** - * @test - */ - public function magicCallMethodReturnsNullInFindOneBySomethingIfQueryReturnsEmptyRawResult(): void - { - $queryResultArray = []; - $this->mockQuery->expects(self::once())->method('equals')->with('foo', 'bar')->willReturn('matchCriteria'); - $this->mockQuery->expects(self::once())->method('matching')->with('matchCriteria')->willReturn($this->mockQuery); - $this->mockQuery->expects(self::once())->method('setLimit')->with(1)->willReturn($this->mockQuery); - $this->mockQuery->expects(self::once())->method('execute')->willReturn($queryResultArray); - self::assertNull($this->repository->findOneByFoo('bar')); - } } diff --git a/typo3/sysext/extbase/Tests/UnitDeprecated/Persistence/RepositoryTest.php b/typo3/sysext/extbase/Tests/UnitDeprecated/Persistence/RepositoryTest.php new file mode 100644 index 0000000000000000000000000000000000000000..5382adbb2aa43f1ea53a4bfb2f24c4f6e4f8d231 --- /dev/null +++ b/typo3/sysext/extbase/Tests/UnitDeprecated/Persistence/RepositoryTest.php @@ -0,0 +1,194 @@ +<?php + +declare(strict_types=1); + +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ + +namespace TYPO3\CMS\Extbase\Tests\UnitDeprecated\Persistence; + +use PHPUnit\Framework\MockObject\MockObject; +use TYPO3\CMS\Extbase\Configuration\ConfigurationManager; +use TYPO3\CMS\Extbase\Persistence\Generic\Backend; +use TYPO3\CMS\Extbase\Persistence\Generic\BackendInterface; +use TYPO3\CMS\Extbase\Persistence\Generic\PersistenceManager; +use TYPO3\CMS\Extbase\Persistence\Generic\QueryFactory; +use TYPO3\CMS\Extbase\Persistence\Generic\QuerySettingsInterface; +use TYPO3\CMS\Extbase\Persistence\Generic\Session; +use TYPO3\CMS\Extbase\Persistence\PersistenceManagerInterface; +use TYPO3\CMS\Extbase\Persistence\QueryInterface; +use TYPO3\CMS\Extbase\Persistence\QueryResultInterface; +use TYPO3\CMS\Extbase\Persistence\Repository; +use TYPO3\TestingFramework\Core\AccessibleObjectInterface; +use TYPO3\TestingFramework\Core\Unit\UnitTestCase; + +class RepositoryTest extends UnitTestCase +{ + /** + * @var Repository|MockObject|AccessibleObjectInterface + */ + protected $repository; + + /** + * @var QueryFactory + */ + protected $mockQueryFactory; + + /** + * @var BackendInterface + */ + protected $mockBackend; + + /** + * @var Session + */ + protected $mockSession; + + /** + * @var PersistenceManagerInterface + */ + protected $mockPersistenceManager; + + /** + * @var QueryInterface + */ + protected $mockQuery; + + /** + * @var QuerySettingsInterface + */ + protected $mockQuerySettings; + + /** + * @var ConfigurationManager + */ + protected $mockConfigurationManager; + + protected function setUp(): void + { + parent::setUp(); + $this->mockQueryFactory = $this->createMock(QueryFactory::class); + $this->mockQuery = $this->createMock(QueryInterface::class); + $this->mockQuerySettings = $this->createMock(QuerySettingsInterface::class); + $this->mockQuery->method('getQuerySettings')->willReturn($this->mockQuerySettings); + $this->mockQueryFactory->method('create')->willReturn($this->mockQuery); + $this->mockSession = $this->createMock(Session::class); + $this->mockConfigurationManager = $this->createMock(ConfigurationManager::class); + $this->mockBackend = $this->getAccessibleMock(Backend::class, null, [$this->mockConfigurationManager], '', false); + $this->mockBackend->_set('session', $this->mockSession); + $this->mockPersistenceManager = $this->getAccessibleMock( + PersistenceManager::class, + ['createQueryForType'], + [ + $this->mockQueryFactory, + $this->mockBackend, + $this->mockSession, + ] + ); + $this->mockBackend->setPersistenceManager($this->mockPersistenceManager); + $this->mockPersistenceManager->method('createQueryForType')->willReturn($this->mockQuery); + $this->repository = $this->getAccessibleMock(Repository::class, null); + $this->repository->injectPersistenceManager($this->mockPersistenceManager); + } + + /** + * @test + */ + public function magicCallMethodAcceptsFindBySomethingCallsAndExecutesAQueryWithThatCriteria(): void + { + $mockQueryResult = $this->createMock(QueryResultInterface::class); + $mockQuery = $this->createMock(QueryInterface::class); + $mockQuery->expects(self::once())->method('equals')->with('foo', 'bar')->willReturn('matchCriteria'); + $mockQuery->expects(self::once())->method('matching')->with('matchCriteria')->willReturn($mockQuery); + $mockQuery->expects(self::once())->method('execute')->with()->willReturn($mockQueryResult); + + $repository = $this->getMockBuilder(Repository::class) + ->onlyMethods(['createQuery']) + ->getMock(); + $repository->expects(self::once())->method('createQuery')->willReturn($mockQuery); + + self::assertSame($mockQueryResult, $repository->findByFoo('bar')); + } + + /** + * @test + */ + public function magicCallMethodAcceptsFindOneBySomethingCallsAndExecutesAQueryWithThatCriteria(): void + { + $object = new \stdClass(); + $mockQueryResult = $this->createMock(QueryResultInterface::class); + $mockQueryResult->expects(self::once())->method('getFirst')->willReturn($object); + $mockQuery = $this->createMock(QueryInterface::class); + $mockQuery->expects(self::once())->method('equals')->with('foo', 'bar')->willReturn('matchCriteria'); + $mockQuery->expects(self::once())->method('matching')->with('matchCriteria')->willReturn($mockQuery); + $mockQuery->expects(self::once())->method('setLimit')->willReturn($mockQuery); + $mockQuery->expects(self::once())->method('execute')->willReturn($mockQueryResult); + + $repository = $this->getMockBuilder(Repository::class) + ->onlyMethods(['createQuery']) + ->getMock(); + $repository->expects(self::once())->method('createQuery')->willReturn($mockQuery); + + self::assertSame($object, $repository->findOneByFoo('bar')); + } + + /** + * @test + */ + public function magicCallMethodAcceptsCountBySomethingCallsAndExecutesAQueryWithThatCriteria(): void + { + $mockQuery = $this->createMock(QueryInterface::class); + $mockQueryResult = $this->createMock(QueryResultInterface::class); + $mockQuery->expects(self::once())->method('equals')->with('foo', 'bar')->willReturn('matchCriteria'); + $mockQuery->expects(self::once())->method('matching')->with('matchCriteria')->willReturn($mockQuery); + $mockQuery->expects(self::once())->method('execute')->willReturn($mockQueryResult); + $mockQueryResult->expects(self::once())->method('count')->willReturn(2); + + $repository = $this->getMockBuilder(Repository::class) + ->onlyMethods(['createQuery']) + ->getMock(); + $repository->expects(self::once())->method('createQuery')->willReturn($mockQuery); + + self::assertSame(2, $repository->countByFoo('bar')); + } + + /** + * @test + */ + public function magicCallMethodReturnsFirstArrayKeyInFindOneBySomethingIfQueryReturnsRawResult(): void + { + $queryResultArray = [ + 0 => [ + 'foo' => 'bar', + ], + ]; + $this->mockQuery->expects(self::once())->method('equals')->with('foo', 'bar')->willReturn('matchCriteria'); + $this->mockQuery->expects(self::once())->method('matching')->with('matchCriteria')->willReturn($this->mockQuery); + $this->mockQuery->expects(self::once())->method('setLimit')->with(1)->willReturn($this->mockQuery); + $this->mockQuery->expects(self::once())->method('execute')->willReturn($queryResultArray); + self::assertSame(['foo' => 'bar'], $this->repository->findOneByFoo('bar')); + } + + /** + * @test + */ + public function magicCallMethodReturnsNullInFindOneBySomethingIfQueryReturnsEmptyRawResult(): void + { + $queryResultArray = []; + $this->mockQuery->expects(self::once())->method('equals')->with('foo', 'bar')->willReturn('matchCriteria'); + $this->mockQuery->expects(self::once())->method('matching')->with('matchCriteria')->willReturn($this->mockQuery); + $this->mockQuery->expects(self::once())->method('setLimit')->with(1)->willReturn($this->mockQuery); + $this->mockQuery->expects(self::once())->method('execute')->willReturn($queryResultArray); + self::assertNull($this->repository->findOneByFoo('bar')); + } +} diff --git a/typo3/sysext/extensionmanager/Classes/Utility/DependencyUtility.php b/typo3/sysext/extensionmanager/Classes/Utility/DependencyUtility.php index 47d755a251f451f46324595673e9fb4dd2b17802..7dde0a0d67895ff7cd4e3ff22184ea22a294b812 100644 --- a/typo3/sysext/extensionmanager/Classes/Utility/DependencyUtility.php +++ b/typo3/sysext/extensionmanager/Classes/Utility/DependencyUtility.php @@ -400,7 +400,7 @@ class DependencyUtility implements SingletonInterface */ protected function isExtensionDownloadableFromRemote(string $extensionKey): bool { - return $this->extensionRepository->countByExtensionKey($extensionKey) > 0; + return $this->extensionRepository->count(['extensionKey' => $extensionKey]) > 0; } /** diff --git a/typo3/sysext/extensionmanager/Tests/Unit/Utility/DependencyUtilityTest.php b/typo3/sysext/extensionmanager/Tests/Unit/Utility/DependencyUtilityTest.php index 9fb34ac29bc6097249754298242b87fd0811a384..bc8cb698227a00600564c88cda0e4217bcf10109 100644 --- a/typo3/sysext/extensionmanager/Tests/Unit/Utility/DependencyUtilityTest.php +++ b/typo3/sysext/extensionmanager/Tests/Unit/Utility/DependencyUtilityTest.php @@ -299,9 +299,9 @@ class DependencyUtilityTest extends UnitTestCase public function isExtensionDownloadableFromRemoteReturnsTrueIfOneVersionExists(): void { $extensionRepositoryMock = $this->getMockBuilder(ExtensionRepository::class) - ->addMethods(['countByExtensionKey']) + ->onlyMethods(['count']) ->getMock(); - $extensionRepositoryMock->expects(self::once())->method('countByExtensionKey')->with('test123')->willReturn(1); + $extensionRepositoryMock->expects(self::once())->method('count')->with(['extensionKey' => 'test123'])->willReturn(1); $dependencyUtility = $this->getAccessibleMock(DependencyUtility::class, null); $dependencyUtility->injectExtensionRepository($extensionRepositoryMock); $count = $dependencyUtility->_call('isExtensionDownloadableFromRemote', 'test123'); @@ -315,9 +315,9 @@ class DependencyUtilityTest extends UnitTestCase public function isExtensionDownloadableFromRemoteReturnsFalseIfNoVersionExists(): void { $extensionRepositoryMock = $this->getMockBuilder(ExtensionRepository::class) - ->addMethods(['countByExtensionKey']) + ->onlyMethods(['count']) ->getMock(); - $extensionRepositoryMock->expects(self::once())->method('countByExtensionKey')->with('test123')->willReturn(0); + $extensionRepositoryMock->expects(self::once())->method('count')->with(['extensionKey' => 'test123'])->willReturn(0); $dependencyUtility = $this->getAccessibleMock(DependencyUtility::class, null); $dependencyUtility->injectExtensionRepository($extensionRepositoryMock); $count = $dependencyUtility->_call('isExtensionDownloadableFromRemote', 'test123');