Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions psalm.xml

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably get rid of psalm in favour of phpstan, as we did in other repositories 🤔

Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
<referencedFunction name="ReflectionClass::getAttributes" />
<file name="src/Bundle/Form/DataTransformer/CollectionToStringTransformer.php" />
<file name="src/Component/src/Doctrine/Persistence/InMemoryRepository.php" />
<file name="src/Component/src/Symfony/Request/State/Provider.php" />
</errorLevel>
</ArgumentTypeCoercion>

Expand Down
104 changes: 104 additions & 0 deletions src/Bundle/Doctrine/ORM/CreatePaginatorTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
<?php

/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace Sylius\Bundle\ResourceBundle\Doctrine\ORM;

use Doctrine\ORM\EntityRepository as DoctrineEntityRepository;
use Doctrine\ORM\QueryBuilder;
use Pagerfanta\Adapter\ArrayAdapter;
use Pagerfanta\Doctrine\ORM\QueryAdapter;
use Pagerfanta\Pagerfanta;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we ready for

Pagerfanta\PagerfantaInterface

yet or it should be kept as it for BC?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems to be ok so, let's go.
https://3v4l.org/O58qm

use Pagerfanta\PagerfantaInterface;
use Sylius\Resource\Model\ResourceInterface;

/**
* @mixin DoctrineEntityRepository
*/
trait CreatePaginatorTrait
Comment on lines +24 to +27

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about @mixin Doctrine\ORM\EntityRepository?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done, indeed.

{
/**
* @return iterable<int, ResourceInterface>
*/
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): PagerfantaInterface
{
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".');
}
Comment on lines +44 to +46

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe move it to the top of ::createPaginator?
No point in applying criteria and sorting when it'll fail either way 🤷

@loic425 loic425 Sep 22, 2025

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that's right, but maybe a user can call the getPaginator without use createPaginator method, cause this method is protected.


// 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): PagerfantaInterface
{
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->getClassMetadata()->getAssociationNames(), $this->getClassMetadata()->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->getClassMetadata()->getAssociationNames(), $this->getClassMetadata()->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;
}
}
91 changes: 4 additions & 87 deletions src/Bundle/Doctrine/ORM/ResourceRepositoryTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,23 +13,16 @@

namespace Sylius\Bundle\ResourceBundle\Doctrine\ORM;

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 Doctrine\ORM\EntityRepository as DoctrineEntityRepository;
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)
* @mixin DoctrineEntityRepository
*/
trait ResourceRepositoryTrait
{
use CreatePaginatorTrait;

public function add(ResourceInterface $resource): void
{
$this->getEntityManager()->persist($resource);
Expand All @@ -43,80 +36,4 @@ public function remove(ResourceInterface $resource): void
$this->getEntityManager()->flush();
}
}

/**
* @return iterable<int, ResourceInterface>
*/
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->getClassMetadata()->getAssociationNames(), $this->getClassMetadata()->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->getClassMetadata()->getAssociationNames(), $this->getClassMetadata()->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;
}
}
2 changes: 1 addition & 1 deletion src/Bundle/Resources/config/services.xml
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@
<service id="Sylius\Bundle\ResourceBundle\Form\Type\DefaultResourceType" alias="sylius.form.type.default" />

<service id="sylius.registry.resource_repository" class="Sylius\Component\Registry\ServiceRegistry" public="false">
<argument>Sylius\Component\Resource\Repository\RepositoryInterface</argument>
<argument>Doctrine\Persistence\ObjectRepository</argument>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feels like a bigger change, is there some explanation why sylius/resource goes away from RepositoryInterface?

What will happen, if my custom repository doesn't implement any resource trait and interface?

@loic425 loic425 Sep 22, 2025

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, the reason is that we do not need to implement that specific interface anymore in the new routing system.
This is the reason why we have this issue (cf first error message on first screenshot).

This RepositoryInterface from Sylius extends that ObjectRepository from Doctrine.
But we do not need these three specific methods required by the Sylius one anymore (add, remove, createPaginator).

<argument>resource repository</argument>
</service>
<service id="sylius.registry.form_builder" class="Sylius\Component\Registry\ServiceRegistry" public="false">
Expand Down
36 changes: 36 additions & 0 deletions src/Component/spec/Symfony/Request/State/ProviderSpec.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,12 @@
use Pagerfanta\Pagerfanta;
use PhpSpec\ObjectBehavior;
use Psr\Container\ContainerInterface;
use Sylius\Bundle\ResourceBundle\Doctrine\ORM\CreatePaginatorTrait;
use Sylius\Component\Resource\Tests\Dummy\RepositoryWithCallables;
use Sylius\Resource\Context\Context;
use Sylius\Resource\Context\Option\RequestOption;
use Sylius\Resource\Doctrine\Persistence\RepositoryInterface;
use Sylius\Resource\Exception\RuntimeException;
use Sylius\Resource\Metadata\Index;
use Sylius\Resource\Metadata\Operation;
use Sylius\Resource\Symfony\ExpressionLanguage\ArgumentParserInterface;
Expand Down Expand Up @@ -172,4 +174,38 @@ function it_calls_repository_as_string_with_specific_repository_method_an_argume
$response = $this->provide($operation, new Context(new RequestOption($request->getWrappedObject())));
$response->shouldReturn($stdClass);
}

function it_throws_an_exception_when_repository_method_does_not_exist(
Operation $operation,
Request $request,
ContainerInterface $locator,
): void {
$operation->getRepository()->willReturn('App\Repository');
$operation->getRepositoryMethod()->willReturn('notFoundMethod');
$operation->getRepositoryArguments()->willReturn(['id' => "request.attributes.get('id')"]);

$locator->has('App\Repository')->willReturn(true);
$locator->get('App\Repository')->willReturn(new \stdClass());

$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);

$this->shouldThrow(new RuntimeException($errorMessage))->during('provide', [$operation, new Context(new RequestOption($request->getWrappedObject()))]);
}

function it_throws_an_exception_when_repository_method_does_not_exist_and_suggest_to_use_create_paginator_if_it_is_appropriated(
Operation $operation,
Request $request,
ContainerInterface $locator,
): void {
$operation->getRepository()->willReturn('App\Repository');
$operation->getRepositoryMethod()->willReturn('createPaginator');
$operation->getRepositoryArguments()->willReturn(['id' => "request.attributes.get('id')"]);

$locator->has('App\Repository')->willReturn(true);
$locator->get('App\Repository')->willReturn(new \stdClass());

$errorMessage = sprintf('Method "createPaginator" not found on repository "%s". You can use the "%s" trait on this repository class.', \stdClass::class, CreatePaginatorTrait::class);

$this->shouldThrow(new RuntimeException($errorMessage))->during('provide', [$operation, new Context(new RequestOption($request->getWrappedObject()))]);
}
}
24 changes: 20 additions & 4 deletions src/Component/src/Symfony/Request/State/Provider.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,12 @@

namespace Sylius\Resource\Symfony\Request\State;

use Pagerfanta\Pagerfanta;
use Pagerfanta\PagerfantaInterface;
use Psr\Container\ContainerInterface;
use Sylius\Bundle\ResourceBundle\Doctrine\ORM\CreatePaginatorTrait;
use Sylius\Resource\Context\Context;
use Sylius\Resource\Context\Option\RequestOption;
use Sylius\Resource\Exception\RuntimeException;
use Sylius\Resource\Metadata\BulkOperationInterface;
use Sylius\Resource\Metadata\CollectionOperationInterface;
use Sylius\Resource\Metadata\Operation;
Expand Down Expand Up @@ -59,14 +61,28 @@ public function provide(Operation $operation, Context $context): object|array|nu
$defaultMethod = 'findById';
}

$method = $operation->getRepositoryMethod() ?? $defaultMethod;
$customMethod = $operation->getRepositoryMethod();
$method = $customMethod ?? $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);

if (
!str_starts_with($method, 'find') && // to allow magic calls on Doctrine repository methods
!\method_exists($repositoryInstance, $method)) {
$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));

if ('createPaginator' === $method) {
$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);
}

throw new RuntimeException($errorMessage);
}

// make it as callable
/** @var callable $repository */
$repository = [$repositoryInstance, $method];
Expand All @@ -91,7 +107,7 @@ public function provide(Operation $operation, Context $context): object|array|nu

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

if ($data instanceof Pagerfanta) {
if ($data instanceof PagerfantaInterface) {
$currentPage = $request->query->getInt('page', 1);
$data->setCurrentPage($currentPage);
}
Expand Down
3 changes: 2 additions & 1 deletion tests/Application/src/Subscription/Entity/Subscription.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
routePrefix: '/admin',
)]
#[Index(grid: 'app_subscription')]
#[Index(shortName: 'withoutGrid', template: 'crud/index.html.twig')]
#[Create]
#[Update]
#[Delete]
Expand Down Expand Up @@ -66,7 +67,7 @@
#[Api\Delete]
#[Api\Get]

#[ORM\Entity]
#[ORM\Entity(repositoryClass: SubscriptionRepository::class)]
class Subscription implements ResourceInterface
{
#[ORM\Column(type: 'string')]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace App\Subscription\Entity;

use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
use Sylius\Bundle\ResourceBundle\Doctrine\ORM\CreatePaginatorTrait;

final class SubscriptionRepository extends ServiceEntityRepository
{
use CreatePaginatorTrait;

public function __construct(
public readonly ManagerRegistry $registry,
) {
parent::__construct($this->registry, Subscription::class);
}
}
Loading
Loading