Skip to content

Commit b1ad9c0

Browse files
committed
Add an error message for non existing repository method
1 parent 6dabe5a commit b1ad9c0

File tree

9 files changed

+229
-120
lines changed

9 files changed

+229
-120
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)