Skip to content
Open
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
13 changes: 11 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,21 @@
## CHANGELOG

## v1.13.1 (2025-09-24)

#### Details

- [#1037](https://github.com/Sylius/SyliusResourceBundle/pull/1037) Add missing event short name on apply state machine transition ([@loic425](https://github.com/loic425))
- [#1045](https://github.com/Sylius/SyliusResourceBundle/pull/1045) Fix - Add missing redirect & redirect arguments ([@loic425](https://github.com/loic425))
- [#1064](https://github.com/Sylius/SyliusResourceBundle/pull/1064) Fix missing behat transliterator package ([@loic425](https://github.com/loic425))
- [#1063](https://github.com/Sylius/SyliusResourceBundle/pull/1063) Add an error message for non existing repository method & suggest to use CreatePaginatorTrait ([@loic425](https://github.com/loic425))

## v1.13.0 (2025-06-04)

#### Details

- [#1025](https://github.com/Sylius/SyliusResourceBundle/pull/1025) Fix implicit nullable parameter + add ECS rule to 1.12 ([@GSadee](https://github.com/GSadee))
- [#1024](https://github.com/Sylius/SyliusResourceBundle/pull/1024) Fix implicit nullable parameter + add ECS rule ([@GSadee](https://github.com/GSadee))

#### Details

## v1.13.0-BETA.1 (2025-05-28)

#### Details
Expand Down
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
],
"require": {
"php": "^8.1",
"behat/transliterator": "^1.2",
"babdev/pagerfanta-bundle": "^4.4",
"doctrine/annotations": "^2.0",
"doctrine/collections": "^2.2",
Expand Down
2 changes: 2 additions & 0 deletions psalm.xml
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,14 @@
<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>

<DeprecatedClass>
<errorLevel type="info">
<referencedClass name="Doctrine\ORM\Event\LifecycleEventArgs" /> <!-- deprecated in doctrine/orm 2.14 -->
<referencedClass name="Gedmo\Sluggable\Util\Urlizer" /> <!-- deprecated in gedmo/doctrine-extensions 3.21 -->
</errorLevel>
<errorLevel type="suppress">
<referencedClass name="Sylius\Component\Resource\Exception\VariantWithNoOptionsValuesException" />
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;
use Pagerfanta\PagerfantaInterface;
use Sylius\Resource\Model\ResourceInterface;

/**
* @mixin DoctrineEntityRepository
*/
trait CreatePaginatorTrait
{
/**
* @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".');
}

// 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>
<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()))]);
}
}
6 changes: 3 additions & 3 deletions src/Component/src/Metadata/ResourceMetadata.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@ public function __construct(
private ?string $pluralName = null,
private ?string $applicationName = null,
private ?string $identifier = null,
protected ?array $normalizationContext = null,
protected ?array $denormalizationContext = null,
protected ?array $validationContext = null,
private ?array $normalizationContext = null,
private ?array $denormalizationContext = null,
private ?array $validationContext = null,
private ?string $class = null,
private string|false|null $driver = null,
private ?array $vars = null,
Expand Down
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
Loading