Skip to content

Commit f4d86d0

Browse files
authored
Add an error message for non existing repository method & suggest to use CreatePaginatorTrait (#1063)
| Q | A | --------------- | ----- | Bug fix? | yes (better error message) | New feature? | | BC breaks? | no | Deprecations? | no | Related tickets | | License | MIT When using the "make:entity", a repository is created automatically, and normally it can be used directly with the new routing system without implementing the Sylius repository interface. But here, in a collection operation, we define this createPaginator by default. So here, first, the idea is to be more precise on what's happening when the default method and suggest using CreatePaginatorTrait. That's another point, but we should also warn (or just fix it) about a non-grid object on the BootstrapAdminUi grid template. Before <img width="1067" height="482" alt="image" src="https://github.com/user-attachments/assets/fc0c2dbf-2ad9-4d87-a517-ba1d91ecb5e1" /> After <img width="1067" height="482" alt="image" src="https://github.com/user-attachments/assets/0dbff1ef-d1d2-411b-8f7a-ef1a132481f1" />
2 parents e92b82a + 5f3251d commit f4d86d0

File tree

9 files changed

+229
-119
lines changed

9 files changed

+229
-119
lines changed

psalm.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
<referencedFunction name="ReflectionClass::getAttributes" />
4141
<file name="src/Bundle/Form/DataTransformer/CollectionToStringTransformer.php" />
4242
<file name="src/Component/src/Doctrine/Persistence/InMemoryRepository.php" />
43+
<file name="src/Component/src/Symfony/Request/State/Provider.php" />
4344
</errorLevel>
4445
</ArgumentTypeCoercion>
4546

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Sylius package.
5+
*
6+
* (c) Sylius Sp. z o.o.
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace Sylius\Bundle\ResourceBundle\Doctrine\ORM;
15+
16+
use Doctrine\ORM\EntityRepository as DoctrineEntityRepository;
17+
use Doctrine\ORM\QueryBuilder;
18+
use Pagerfanta\Adapter\ArrayAdapter;
19+
use Pagerfanta\Doctrine\ORM\QueryAdapter;
20+
use Pagerfanta\Pagerfanta;
21+
use Pagerfanta\PagerfantaInterface;
22+
use Sylius\Resource\Model\ResourceInterface;
23+
24+
/**
25+
* @mixin DoctrineEntityRepository
26+
*/
27+
trait CreatePaginatorTrait
28+
{
29+
/**
30+
* @return iterable<int, ResourceInterface>
31+
*/
32+
public function createPaginator(array $criteria = [], array $sorting = []): iterable
33+
{
34+
$queryBuilder = $this->createQueryBuilder('o');
35+
36+
$this->applyCriteria($queryBuilder, $criteria);
37+
$this->applySorting($queryBuilder, $sorting);
38+
39+
return $this->getPaginator($queryBuilder);
40+
}
41+
42+
protected function getPaginator(QueryBuilder $queryBuilder): PagerfantaInterface
43+
{
44+
if (!class_exists(QueryAdapter::class)) {
45+
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".');
46+
}
47+
48+
// Use output walkers option in the query adapter should be false as it affects performance greatly (see sylius/sylius#3775)
49+
return new Pagerfanta(new QueryAdapter($queryBuilder, false, false));
50+
}
51+
52+
/**
53+
* @param array $objects
54+
*/
55+
protected function getArrayPaginator($objects): PagerfantaInterface
56+
{
57+
return new Pagerfanta(new ArrayAdapter($objects));
58+
}
59+
60+
protected function applyCriteria(QueryBuilder $queryBuilder, array $criteria = []): void
61+
{
62+
foreach ($criteria as $property => $value) {
63+
if (!in_array($property, array_merge($this->getClassMetadata()->getAssociationNames(), $this->getClassMetadata()->getFieldNames()), true)) {
64+
continue;
65+
}
66+
67+
$name = $this->getPropertyName($property);
68+
69+
if (null === $value) {
70+
$queryBuilder->andWhere($queryBuilder->expr()->isNull($name));
71+
} elseif (is_array($value)) {
72+
$queryBuilder->andWhere($queryBuilder->expr()->in($name, $value));
73+
} elseif ('' !== $value) {
74+
$parameter = str_replace('.', '_', $property);
75+
$queryBuilder
76+
->andWhere($queryBuilder->expr()->eq($name, ':' . $parameter))
77+
->setParameter($parameter, $value)
78+
;
79+
}
80+
}
81+
}
82+
83+
protected function applySorting(QueryBuilder $queryBuilder, array $sorting = []): void
84+
{
85+
foreach ($sorting as $property => $order) {
86+
if (!in_array($property, array_merge($this->getClassMetadata()->getAssociationNames(), $this->getClassMetadata()->getFieldNames()), true)) {
87+
continue;
88+
}
89+
90+
if (!empty($order)) {
91+
$queryBuilder->addOrderBy($this->getPropertyName($property), $order);
92+
}
93+
}
94+
}
95+
96+
protected function getPropertyName(string $name): string
97+
{
98+
if (!str_contains($name, '.')) {
99+
return 'o' . '.' . $name;
100+
}
101+
102+
return $name;
103+
}
104+
}

src/Bundle/Doctrine/ORM/ResourceRepositoryTrait.php

Lines changed: 4 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -13,23 +13,16 @@
1313

1414
namespace Sylius\Bundle\ResourceBundle\Doctrine\ORM;
1515

16-
use Doctrine\ORM\EntityManagerInterface;
17-
use Doctrine\ORM\Mapping\ClassMetadata;
18-
use Doctrine\ORM\QueryBuilder;
19-
use Pagerfanta\Adapter\ArrayAdapter;
20-
use Pagerfanta\Doctrine\ORM\QueryAdapter;
21-
use Pagerfanta\Pagerfanta;
16+
use Doctrine\ORM\EntityRepository as DoctrineEntityRepository;
2217
use Sylius\Resource\Model\ResourceInterface;
2318

2419
/**
25-
* @property EntityManagerInterface $_em
26-
* @property ClassMetadata $_class
27-
*
28-
* @method QueryBuilder createQueryBuilder(string $alias, string $indexBy = null)
29-
* @method ?object find($id, $lockMode = null, $lockVersion = null)
20+
* @mixin DoctrineEntityRepository
3021
*/
3122
trait ResourceRepositoryTrait
3223
{
24+
use CreatePaginatorTrait;
25+
3326
public function add(ResourceInterface $resource): void
3427
{
3528
$this->getEntityManager()->persist($resource);
@@ -43,80 +36,4 @@ public function remove(ResourceInterface $resource): void
4336
$this->getEntityManager()->flush();
4437
}
4538
}
46-
47-
/**
48-
* @return iterable<int, ResourceInterface>
49-
*/
50-
public function createPaginator(array $criteria = [], array $sorting = []): iterable
51-
{
52-
$queryBuilder = $this->createQueryBuilder('o');
53-
54-
$this->applyCriteria($queryBuilder, $criteria);
55-
$this->applySorting($queryBuilder, $sorting);
56-
57-
return $this->getPaginator($queryBuilder);
58-
}
59-
60-
protected function getPaginator(QueryBuilder $queryBuilder): Pagerfanta
61-
{
62-
if (!class_exists(QueryAdapter::class)) {
63-
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".');
64-
}
65-
66-
// Use output walkers option in the query adapter should be false as it affects performance greatly (see sylius/sylius#3775)
67-
return new Pagerfanta(new QueryAdapter($queryBuilder, false, false));
68-
}
69-
70-
/**
71-
* @param array $objects
72-
*/
73-
protected function getArrayPaginator($objects): Pagerfanta
74-
{
75-
return new Pagerfanta(new ArrayAdapter($objects));
76-
}
77-
78-
protected function applyCriteria(QueryBuilder $queryBuilder, array $criteria = []): void
79-
{
80-
foreach ($criteria as $property => $value) {
81-
if (!in_array($property, array_merge($this->getClassMetadata()->getAssociationNames(), $this->getClassMetadata()->getFieldNames()), true)) {
82-
continue;
83-
}
84-
85-
$name = $this->getPropertyName($property);
86-
87-
if (null === $value) {
88-
$queryBuilder->andWhere($queryBuilder->expr()->isNull($name));
89-
} elseif (is_array($value)) {
90-
$queryBuilder->andWhere($queryBuilder->expr()->in($name, $value));
91-
} elseif ('' !== $value) {
92-
$parameter = str_replace('.', '_', $property);
93-
$queryBuilder
94-
->andWhere($queryBuilder->expr()->eq($name, ':' . $parameter))
95-
->setParameter($parameter, $value)
96-
;
97-
}
98-
}
99-
}
100-
101-
protected function applySorting(QueryBuilder $queryBuilder, array $sorting = []): void
102-
{
103-
foreach ($sorting as $property => $order) {
104-
if (!in_array($property, array_merge($this->getClassMetadata()->getAssociationNames(), $this->getClassMetadata()->getFieldNames()), true)) {
105-
continue;
106-
}
107-
108-
if (!empty($order)) {
109-
$queryBuilder->addOrderBy($this->getPropertyName($property), $order);
110-
}
111-
}
112-
}
113-
114-
protected function getPropertyName(string $name): string
115-
{
116-
if (false === strpos($name, '.')) {
117-
return 'o' . '.' . $name;
118-
}
119-
120-
return $name;
121-
}
12239
}

src/Bundle/Resources/config/services.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@
6969
<service id="Sylius\Bundle\ResourceBundle\Form\Type\DefaultResourceType" alias="sylius.form.type.default" />
7070

7171
<service id="sylius.registry.resource_repository" class="Sylius\Component\Registry\ServiceRegistry" public="false">
72-
<argument>Sylius\Component\Resource\Repository\RepositoryInterface</argument>
72+
<argument>Doctrine\Persistence\ObjectRepository</argument>
7373
<argument>resource repository</argument>
7474
</service>
7575
<service id="sylius.registry.form_builder" class="Sylius\Component\Registry\ServiceRegistry" public="false">

src/Component/spec/Symfony/Request/State/ProviderSpec.php

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,12 @@
1616
use Pagerfanta\Pagerfanta;
1717
use PhpSpec\ObjectBehavior;
1818
use Psr\Container\ContainerInterface;
19+
use Sylius\Bundle\ResourceBundle\Doctrine\ORM\CreatePaginatorTrait;
1920
use Sylius\Component\Resource\Tests\Dummy\RepositoryWithCallables;
2021
use Sylius\Resource\Context\Context;
2122
use Sylius\Resource\Context\Option\RequestOption;
2223
use Sylius\Resource\Doctrine\Persistence\RepositoryInterface;
24+
use Sylius\Resource\Exception\RuntimeException;
2325
use Sylius\Resource\Metadata\Index;
2426
use Sylius\Resource\Metadata\Operation;
2527
use Sylius\Resource\Symfony\ExpressionLanguage\ArgumentParserInterface;
@@ -172,4 +174,38 @@ function it_calls_repository_as_string_with_specific_repository_method_an_argume
172174
$response = $this->provide($operation, new Context(new RequestOption($request->getWrappedObject())));
173175
$response->shouldReturn($stdClass);
174176
}
177+
178+
function it_throws_an_exception_when_repository_method_does_not_exist(
179+
Operation $operation,
180+
Request $request,
181+
ContainerInterface $locator,
182+
): void {
183+
$operation->getRepository()->willReturn('App\Repository');
184+
$operation->getRepositoryMethod()->willReturn('notFoundMethod');
185+
$operation->getRepositoryArguments()->willReturn(['id' => "request.attributes.get('id')"]);
186+
187+
$locator->has('App\Repository')->willReturn(true);
188+
$locator->get('App\Repository')->willReturn(new \stdClass());
189+
190+
$errorMessage = sprintf('Method "notFoundMethod" not found on repository "%s". You can either add it or configure another one in the repositoryMethod option for your operation.', \stdClass::class);
191+
192+
$this->shouldThrow(new RuntimeException($errorMessage))->during('provide', [$operation, new Context(new RequestOption($request->getWrappedObject()))]);
193+
}
194+
195+
function it_throws_an_exception_when_repository_method_does_not_exist_and_suggest_to_use_create_paginator_if_it_is_appropriated(
196+
Operation $operation,
197+
Request $request,
198+
ContainerInterface $locator,
199+
): void {
200+
$operation->getRepository()->willReturn('App\Repository');
201+
$operation->getRepositoryMethod()->willReturn('createPaginator');
202+
$operation->getRepositoryArguments()->willReturn(['id' => "request.attributes.get('id')"]);
203+
204+
$locator->has('App\Repository')->willReturn(true);
205+
$locator->get('App\Repository')->willReturn(new \stdClass());
206+
207+
$errorMessage = sprintf('Method "createPaginator" not found on repository "%s". You can use the "%s" trait on this repository class.', \stdClass::class, CreatePaginatorTrait::class);
208+
209+
$this->shouldThrow(new RuntimeException($errorMessage))->during('provide', [$operation, new Context(new RequestOption($request->getWrappedObject()))]);
210+
}
175211
}

src/Component/src/Symfony/Request/State/Provider.php

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,12 @@
1313

1414
namespace Sylius\Resource\Symfony\Request\State;
1515

16-
use Pagerfanta\Pagerfanta;
16+
use Pagerfanta\PagerfantaInterface;
1717
use Psr\Container\ContainerInterface;
18+
use Sylius\Bundle\ResourceBundle\Doctrine\ORM\CreatePaginatorTrait;
1819
use Sylius\Resource\Context\Context;
1920
use Sylius\Resource\Context\Option\RequestOption;
21+
use Sylius\Resource\Exception\RuntimeException;
2022
use Sylius\Resource\Metadata\BulkOperationInterface;
2123
use Sylius\Resource\Metadata\CollectionOperationInterface;
2224
use Sylius\Resource\Metadata\Operation;
@@ -59,14 +61,28 @@ public function provide(Operation $operation, Context $context): object|array|nu
5961
$defaultMethod = 'findById';
6062
}
6163

62-
$method = $operation->getRepositoryMethod() ?? $defaultMethod;
64+
$customMethod = $operation->getRepositoryMethod();
65+
$method = $customMethod ?? $defaultMethod;
6366

6467
if (!$this->locator->has($repository)) {
65-
throw new \RuntimeException(sprintf('Repository "%s" not found on operation "%s"', $repository, $operation->getName() ?? ''));
68+
throw new RuntimeException(sprintf('Repository "%s" not found on operation "%s".', $repository, $operation->getName() ?? ''));
6669
}
6770

71+
/** @var object $repositoryInstance */
6872
$repositoryInstance = $this->locator->get($repository);
6973

74+
if (
75+
!str_starts_with($method, 'find') && // to allow magic calls on Doctrine repository methods
76+
!\method_exists($repositoryInstance, $method)) {
77+
$errorMessage = sprintf('Method "%s" not found on repository "%s". You can either add it or configure another one in the repositoryMethod option for your operation.', $method, get_debug_type($repositoryInstance));
78+
79+
if ('createPaginator' === $method) {
80+
$errorMessage = sprintf('Method "%s" not found on repository "%s". You can use the "%s" trait on this repository class.', $method, get_debug_type($repositoryInstance), CreatePaginatorTrait::class);
81+
}
82+
83+
throw new RuntimeException($errorMessage);
84+
}
85+
7086
// make it as callable
7187
/** @var callable $repository */
7288
$repository = [$repositoryInstance, $method];
@@ -91,7 +107,7 @@ public function provide(Operation $operation, Context $context): object|array|nu
91107

92108
$data = $repository(...$arguments);
93109

94-
if ($data instanceof Pagerfanta) {
110+
if ($data instanceof PagerfantaInterface) {
95111
$currentPage = $request->query->getInt('page', 1);
96112
$data->setCurrentPage($currentPage);
97113
}

tests/Application/src/Subscription/Entity/Subscription.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
routePrefix: '/admin',
3838
)]
3939
#[Index(grid: 'app_subscription')]
40+
#[Index(shortName: 'withoutGrid', template: 'crud/index.html.twig')]
4041
#[Create]
4142
#[Update]
4243
#[Delete]
@@ -66,7 +67,7 @@
6667
#[Api\Delete]
6768
#[Api\Get]
6869

69-
#[ORM\Entity]
70+
#[ORM\Entity(repositoryClass: SubscriptionRepository::class)]
7071
class Subscription implements ResourceInterface
7172
{
7273
#[ORM\Column(type: 'string')]
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Sylius package.
5+
*
6+
* (c) Sylius Sp. z o.o.
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace App\Subscription\Entity;
15+
16+
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
17+
use Doctrine\Persistence\ManagerRegistry;
18+
use Sylius\Bundle\ResourceBundle\Doctrine\ORM\CreatePaginatorTrait;
19+
20+
final class SubscriptionRepository extends ServiceEntityRepository
21+
{
22+
use CreatePaginatorTrait;
23+
24+
public function __construct(
25+
public readonly ManagerRegistry $registry,
26+
) {
27+
parent::__construct($this->registry, Subscription::class);
28+
}
29+
}

0 commit comments

Comments
 (0)