diff --git a/composer.json b/composer.json index 76debadd2..769455f95 100644 --- a/composer.json +++ b/composer.json @@ -74,6 +74,7 @@ "sylius-labs/coding-standard": "^4.0", "sylius/grid-bundle": "^1.7 || v1.12.0-ALPHA.1", "symfony/dependency-injection": "^5.4 || ^6.0", + "symfony/console": "^5.4 || ^6.0", "symfony/dotenv": "^5.4 || ^6.0", "symfony/stopwatch": "^5.4 || ^6.0", "symfony/uid": "^5.4 || ^6.0", diff --git a/docs/create_new_resource.md b/docs/create_new_resource.md index 80b0eec74..6d7c53aec 100644 --- a/docs/create_new_resource.md +++ b/docs/create_new_resource.md @@ -124,10 +124,9 @@ class BookRepository extends ServiceEntityRepository } ``` -The generated code is not compatible with Sylius Resource yet, so we need to make few changes. +The generated code could be not compatible with Sylius Resource in some cases, so we need to make few changes. -* First, your repository should implement the `Sylius\Component\Resource\Repository\RepositoryInterface` interface -* Then, add the `Sylius\Bundle\ResourceBundle\Doctrine\ORM\ResourceRepositoryTrait` trait +* Add the `Sylius\Bundle\ResourceBundle\Doctrine\ORM\PaginatedRepositoryTrait` trait Your repository should look like this: @@ -139,7 +138,7 @@ namespace App\Repository; use App\Entity\Book; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; use Doctrine\Persistence\ManagerRegistry; -use Sylius\Bundle\ResourceBundle\Doctrine\ORM\ResourceRepositoryTrait; +use Sylius\Bundle\ResourceBundle\Doctrine\ORM\PaginatedRepositoryTrait; use Sylius\Resource\Doctrine\Persistence\RepositoryInterface; /** @@ -150,9 +149,9 @@ use Sylius\Resource\Doctrine\Persistence\RepositoryInterface; * @method Book[] findAll() * @method Book[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) */ -class BookRepository extends ServiceEntityRepository implements RepositoryInterface +class BookRepository extends ServiceEntityRepository { - use ResourceRepositoryTrait; + use PaginatedRepositoryTrait; public function __construct(ManagerRegistry $registry) { diff --git a/src/Bundle/DependencyInjection/Compiler/RegisterResourceRepositoryPass.php b/src/Bundle/DependencyInjection/Compiler/RegisterResourceRepositoryPass.php index 99bf62f49..76f0026b0 100644 --- a/src/Bundle/DependencyInjection/Compiler/RegisterResourceRepositoryPass.php +++ b/src/Bundle/DependencyInjection/Compiler/RegisterResourceRepositoryPass.php @@ -13,6 +13,7 @@ namespace Sylius\Bundle\ResourceBundle\DependencyInjection\Compiler; +use Sylius\Resource\Doctrine\Persistence\RepositoryInterface; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Reference; @@ -35,6 +36,13 @@ public function process(ContainerBuilder $container): void $repositoryId = sprintf('%s.repository.%s', $applicationName, $resourceName); if ($container->has($repositoryId)) { + $repositoryDefinition = $container->findDefinition($repositoryId); + + // Do not register repositories that do not implement the Sylius repository interface + if (!\is_a($repositoryDefinition->getClass() ?? '', RepositoryInterface::class, true)) { + continue; + } + $repositoryRegistry->addMethodCall('register', [$alias, new Reference($repositoryId)]); } } diff --git a/src/Bundle/DependencyInjection/Driver/Doctrine/DoctrineORMDriver.php b/src/Bundle/DependencyInjection/Driver/Doctrine/DoctrineORMDriver.php index c0393169b..e6c9f53d4 100644 --- a/src/Bundle/DependencyInjection/Driver/Doctrine/DoctrineORMDriver.php +++ b/src/Bundle/DependencyInjection/Driver/Doctrine/DoctrineORMDriver.php @@ -18,6 +18,7 @@ use Doctrine\Common\Persistence\ObjectManager as DeprecatedObjectManager; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Mapping\ClassMetadata; +use Doctrine\ORM\Mapping\Entity; use Doctrine\Persistence\ObjectManager; use Sylius\Bundle\ResourceBundle\Doctrine\ORM\EntityRepository; use Sylius\Bundle\ResourceBundle\SyliusResourceBundle; @@ -54,6 +55,14 @@ protected function addRepository(ContainerBuilder $container, MetadataInterface $repositoryClass = $metadata->getClass('repository'); } + $entityAttributeRepositoryClass = (new \ReflectionClass($metadata->getClass('model'))) + ->getAttributes(Entity::class)[0] + ->newInstance()->repositoryClass; + + if (null !== $entityAttributeRepositoryClass) { + $repositoryClass = $entityAttributeRepositoryClass; + } + $serviceId = $metadata->getServiceId('repository'); $managerReference = new Reference($metadata->getServiceId('manager')); $definition = new Definition($repositoryClass); @@ -73,7 +82,6 @@ protected function addRepository(ContainerBuilder $container, MetadataInterface } else { if (is_a($repositoryClass, ServiceEntityRepository::class, true)) { $definition->setArguments([new Reference('doctrine')]); - $container->setDefinition($serviceId, $definition); } else { $definition->setArguments([$managerReference, $this->getClassMetadataDefinition($metadata)]); } diff --git a/src/Bundle/Doctrine/ORM/PaginatedRepositoryTrait.php b/src/Bundle/Doctrine/ORM/PaginatedRepositoryTrait.php new file mode 100644 index 000000000..fb39657a6 --- /dev/null +++ b/src/Bundle/Doctrine/ORM/PaginatedRepositoryTrait.php @@ -0,0 +1,107 @@ + + */ + public function createPaginator(array $criteria = [], array $sorting = []): iterable + { + $queryBuilder = $this->createQueryBuilder('o'); + + $this->applyCriteria($queryBuilder, $criteria); + $this->applySorting($queryBuilder, $sorting); + + return $this->getPaginator($queryBuilder); + } + + protected function getPaginator(QueryBuilder $queryBuilder): Pagerfanta + { + if (!class_exists(QueryAdapter::class)) { + throw new \LogicException('You can not use the "paginator" if Pargefanta Doctrine ORM Adapter is not available. Try running "composer require pagerfanta/doctrine-orm-adapter".'); + } + + // Use output walkers option in the query adapter should be false as it affects performance greatly (see sylius/sylius#3775) + return new Pagerfanta(new QueryAdapter($queryBuilder, false, false)); + } + + /** + * @param array $objects + */ + protected function getArrayPaginator($objects): Pagerfanta + { + return new Pagerfanta(new ArrayAdapter($objects)); + } + + protected function applyCriteria(QueryBuilder $queryBuilder, array $criteria = []): void + { + foreach ($criteria as $property => $value) { + if (!in_array($property, array_merge($this->_class->getAssociationNames(), $this->_class->getFieldNames()), true)) { + continue; + } + + $name = $this->getPropertyName($property); + + if (null === $value) { + $queryBuilder->andWhere($queryBuilder->expr()->isNull($name)); + } elseif (is_array($value)) { + $queryBuilder->andWhere($queryBuilder->expr()->in($name, $value)); + } elseif ('' !== $value) { + $parameter = str_replace('.', '_', $property); + $queryBuilder + ->andWhere($queryBuilder->expr()->eq($name, ':' . $parameter)) + ->setParameter($parameter, $value) + ; + } + } + } + + protected function applySorting(QueryBuilder $queryBuilder, array $sorting = []): void + { + foreach ($sorting as $property => $order) { + if (!in_array($property, array_merge($this->_class->getAssociationNames(), $this->_class->getFieldNames()), true)) { + continue; + } + + if (!empty($order)) { + $queryBuilder->addOrderBy($this->getPropertyName($property), $order); + } + } + } + + protected function getPropertyName(string $name): string + { + if (!str_contains($name, '.')) { + return 'o' . '.' . $name; + } + + return $name; + } +} diff --git a/src/Bundle/Doctrine/ORM/ResourceRepositoryTrait.php b/src/Bundle/Doctrine/ORM/ResourceRepositoryTrait.php index 1ad59dded..8c79acab5 100644 --- a/src/Bundle/Doctrine/ORM/ResourceRepositoryTrait.php +++ b/src/Bundle/Doctrine/ORM/ResourceRepositoryTrait.php @@ -15,21 +15,18 @@ use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Mapping\ClassMetadata; -use Doctrine\ORM\QueryBuilder; -use Pagerfanta\Adapter\ArrayAdapter; -use Pagerfanta\Doctrine\ORM\QueryAdapter; -use Pagerfanta\Pagerfanta; use Sylius\Resource\Model\ResourceInterface; /** * @property EntityManagerInterface $_em * @property ClassMetadata $_class * - * @method QueryBuilder createQueryBuilder(string $alias, string $indexBy = null) * @method ?object find($id, $lockMode = null, $lockVersion = null) */ trait ResourceRepositoryTrait { + use PaginatedRepositoryTrait; + public function add(ResourceInterface $resource): void { $this->_em->persist($resource); @@ -43,80 +40,4 @@ public function remove(ResourceInterface $resource): void $this->_em->flush(); } } - - /** - * @return iterable - */ - public function createPaginator(array $criteria = [], array $sorting = []): iterable - { - $queryBuilder = $this->createQueryBuilder('o'); - - $this->applyCriteria($queryBuilder, $criteria); - $this->applySorting($queryBuilder, $sorting); - - return $this->getPaginator($queryBuilder); - } - - protected function getPaginator(QueryBuilder $queryBuilder): Pagerfanta - { - if (!class_exists(QueryAdapter::class)) { - throw new \LogicException('You can not use the "paginator" if Pargefanta Doctrine ORM Adapter is not available. Try running "composer require pagerfanta/doctrine-orm-adapter".'); - } - - // Use output walkers option in the query adapter should be false as it affects performance greatly (see sylius/sylius#3775) - return new Pagerfanta(new QueryAdapter($queryBuilder, false, false)); - } - - /** - * @param array $objects - */ - protected function getArrayPaginator($objects): Pagerfanta - { - return new Pagerfanta(new ArrayAdapter($objects)); - } - - protected function applyCriteria(QueryBuilder $queryBuilder, array $criteria = []): void - { - foreach ($criteria as $property => $value) { - if (!in_array($property, array_merge($this->_class->getAssociationNames(), $this->_class->getFieldNames()), true)) { - continue; - } - - $name = $this->getPropertyName($property); - - if (null === $value) { - $queryBuilder->andWhere($queryBuilder->expr()->isNull($name)); - } elseif (is_array($value)) { - $queryBuilder->andWhere($queryBuilder->expr()->in($name, $value)); - } elseif ('' !== $value) { - $parameter = str_replace('.', '_', $property); - $queryBuilder - ->andWhere($queryBuilder->expr()->eq($name, ':' . $parameter)) - ->setParameter($parameter, $value) - ; - } - } - } - - protected function applySorting(QueryBuilder $queryBuilder, array $sorting = []): void - { - foreach ($sorting as $property => $order) { - if (!in_array($property, array_merge($this->_class->getAssociationNames(), $this->_class->getFieldNames()), true)) { - continue; - } - - if (!empty($order)) { - $queryBuilder->addOrderBy($this->getPropertyName($property), $order); - } - } - } - - protected function getPropertyName(string $name): string - { - if (false === strpos($name, '.')) { - return 'o' . '.' . $name; - } - - return $name; - } } diff --git a/src/Component/spec/Symfony/Request/State/ProviderSpec.php b/src/Component/spec/Symfony/Request/State/ProviderSpec.php index 22d5fca3c..1e2120dd5 100644 --- a/src/Component/spec/Symfony/Request/State/ProviderSpec.php +++ b/src/Component/spec/Symfony/Request/State/ProviderSpec.php @@ -64,6 +64,7 @@ function it_calls_repository_as_string( RepositoryInterface $repository, \stdClass $stdClass, ): void { + $operation->getName()->willReturn('operation_name'); $operation->getRepository()->willReturn('App\Repository'); $operation->getRepositoryMethod()->willReturn(null); $operation->getRepositoryArguments()->willReturn(null); @@ -133,6 +134,7 @@ function it_calls_repository_as_string_with_specific_repository_method( RepositoryInterface $repository, \stdClass $stdClass, ): void { + $operation->getName()->willReturn('operation_name'); $operation->getRepository()->willReturn('App\Repository'); $operation->getRepositoryMethod()->willReturn('find'); $operation->getRepositoryArguments()->willReturn(null); @@ -158,6 +160,7 @@ function it_calls_repository_as_string_with_specific_repository_method_an_argume ArgumentParserInterface $argumentParser, \stdClass $stdClass, ): void { + $operation->getName()->willReturn('operation_name'); $operation->getRepository()->willReturn('App\Repository'); $operation->getRepositoryMethod()->willReturn('find'); $operation->getRepositoryArguments()->willReturn(['id' => "request.attributes.get('id')"]); diff --git a/src/Component/src/Symfony/Request/State/Provider.php b/src/Component/src/Symfony/Request/State/Provider.php index fe7fdd26d..4bb11a01d 100644 --- a/src/Component/src/Symfony/Request/State/Provider.php +++ b/src/Component/src/Symfony/Request/State/Provider.php @@ -24,6 +24,7 @@ use Sylius\Resource\State\ProviderInterface; use Sylius\Resource\Symfony\ExpressionLanguage\ArgumentParserInterface; use Sylius\Resource\Symfony\Request\RepositoryArgumentResolver; +use Webmozart\Assert\Assert; final class Provider implements ProviderInterface { @@ -59,11 +60,22 @@ public function provide(Operation $operation, Context $context): object|array|nu $method = $operation->getRepositoryMethod() ?? $defaultMethod; if (!$this->locator->has($repository)) { - throw new \RuntimeException(sprintf('Repository "%s" not found on operation "%s"', $repository, $operation->getName() ?? '')); + throw new \RuntimeException(sprintf('Repository "%s" not found on operation "%s".', $repository, $operation->getName() ?? '')); } + /** @var object $repositoryInstance */ $repositoryInstance = $this->locator->get($repository); + Assert::true( + (new \ReflectionClass($repositoryInstance))->hasMethod($method), + sprintf( + 'Repository "%s" does not contain "%s" method. Configure it or configure another repository method in your operation "%s".', + $repositoryInstance::class, + $method, + $operation->getName() ?? '', + ), + ); + // make it as callable /** @var callable $repository */ $repository = [$repositoryInstance, $method]; diff --git a/tests/Application/config/packages/doctrine.yaml b/tests/Application/config/packages/doctrine.yaml index 5c208b673..86654264c 100644 --- a/tests/Application/config/packages/doctrine.yaml +++ b/tests/Application/config/packages/doctrine.yaml @@ -19,6 +19,11 @@ doctrine: type: attribute dir: '%kernel.project_dir%/src/BoardGameBlog/Domain' prefix: 'App\BoardGameBlog\Domain' + Calendar: + is_bundle: false + type: attribute + dir: '%kernel.project_dir%/src/Calendar/Entity' + prefix: 'App\Calendar\Entity' Subscription: is_bundle: false type: attribute diff --git a/tests/Application/config/services.yaml b/tests/Application/config/services.yaml index bc87b38e3..81a65e2ad 100644 --- a/tests/Application/config/services.yaml +++ b/tests/Application/config/services.yaml @@ -88,6 +88,9 @@ services: App\BoardGameBlog\: resource: '../src/BoardGameBlog' + App\Calendar\: + resource: '../src/Calendar' + App\Subscription\: resource: '../src/Subscription' diff --git a/tests/Application/config/sylius/resources.yaml b/tests/Application/config/sylius/resources.yaml index d5dec35e1..bf2806f35 100644 --- a/tests/Application/config/sylius/resources.yaml +++ b/tests/Application/config/sylius/resources.yaml @@ -2,6 +2,7 @@ sylius_resource: mapping: paths: - '%kernel.project_dir%/src/BoardGameBlog/Infrastructure/Sylius/Resource' + - '%kernel.project_dir%/src/Calendar/Entity' - '%kernel.project_dir%/src/Subscription/Entity' translation: diff --git a/tests/Application/src/Calendar/Entity/Event.php b/tests/Application/src/Calendar/Entity/Event.php new file mode 100644 index 000000000..7b1d91dbe --- /dev/null +++ b/tests/Application/src/Calendar/Entity/Event.php @@ -0,0 +1,47 @@ +id; + } +} diff --git a/tests/Application/src/Calendar/Repository/EventRepository.php b/tests/Application/src/Calendar/Repository/EventRepository.php new file mode 100644 index 000000000..0eab725a3 --- /dev/null +++ b/tests/Application/src/Calendar/Repository/EventRepository.php @@ -0,0 +1,37 @@ + + * + * @method Event|null find($id, $lockMode = null, $lockVersion = null) + * @method Event|null findOneBy(array $criteria, array $orderBy = null) + * @method Event[] findAll() + * @method Event[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) + */ +final class EventRepository extends ServiceEntityRepository +{ + use PaginatedRepositoryTrait; + + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, Event::class); + } +} diff --git a/tests/Application/src/Tests/Controller/EventUiTest.php b/tests/Application/src/Tests/Controller/EventUiTest.php new file mode 100644 index 000000000..32011d428 --- /dev/null +++ b/tests/Application/src/Tests/Controller/EventUiTest.php @@ -0,0 +1,41 @@ +loadFixturesFromFile('events.yml'); + + $this->client->request('GET', '/admin/events'); + $response = $this->client->getResponse(); + + $this->assertResponseCode($response, Response::HTTP_OK); + $content = $response->getContent(); + + $this->assertStringContainsString('
  • Name: Sylius Con 2023
  • ', $content); + } + + protected function buildMatcher(): Matcher + { + return $this->matcherFactory->createMatcher(new VoidBacktrace()); + } +} diff --git a/tests/Application/src/Tests/DataFixtures/ORM/events.yml b/tests/Application/src/Tests/DataFixtures/ORM/events.yml new file mode 100644 index 000000000..7d888c2cd --- /dev/null +++ b/tests/Application/src/Tests/DataFixtures/ORM/events.yml @@ -0,0 +1,3 @@ +App\Calendar\Entity\Event: + sylius_con_2023: + name: 'Sylius Con 2023' diff --git a/tests/Application/templates/calendar/event/index.html.twig b/tests/Application/templates/calendar/event/index.html.twig new file mode 100644 index 000000000..78e87ca37 --- /dev/null +++ b/tests/Application/templates/calendar/event/index.html.twig @@ -0,0 +1,7 @@ +

    Events

    + +