├── .gitignore ├── OptionsInterface.php ├── Exception └── ProviderNotFoundException.php ├── SerializerAwareProviderInterface.php ├── Pagination ├── PartialPaginatorInterface.php ├── PaginatorInterface.php ├── TraversablePaginator.php ├── ArrayPaginator.php ├── PaginationOptions.php └── Pagination.php ├── ProcessorInterface.php ├── phpunit.xml.dist ├── ProviderInterface.php ├── SerializerAwareProviderTrait.php ├── LICENSE ├── ObjectProvider.php ├── CallableProcessor.php ├── CallableProvider.php ├── composer.json ├── UriVariablesResolverTrait.php └── CreateProvider.php /.gitignore: -------------------------------------------------------------------------------- 1 | /composer.lock 2 | /vendor 3 | /.phpunit.result.cache 4 | -------------------------------------------------------------------------------- /OptionsInterface.php: -------------------------------------------------------------------------------- 1 | 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 ApiPlatform\State; 15 | 16 | interface OptionsInterface 17 | { 18 | } 19 | -------------------------------------------------------------------------------- /Exception/ProviderNotFoundException.php: -------------------------------------------------------------------------------- 1 | 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 ApiPlatform\State\Exception; 15 | 16 | use ApiPlatform\Metadata\Exception\RuntimeException; 17 | 18 | final class ProviderNotFoundException extends RuntimeException 19 | { 20 | } 21 | -------------------------------------------------------------------------------- /SerializerAwareProviderInterface.php: -------------------------------------------------------------------------------- 1 | 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 ApiPlatform\State; 15 | 16 | use Psr\Container\ContainerInterface; 17 | 18 | /** 19 | * Injects serializer in providers. 20 | * 21 | * @author Vincent Chalamon 22 | */ 23 | interface SerializerAwareProviderInterface 24 | { 25 | public function setSerializerLocator(ContainerInterface $serializerLocator); 26 | } 27 | -------------------------------------------------------------------------------- /Pagination/PartialPaginatorInterface.php: -------------------------------------------------------------------------------- 1 | 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 ApiPlatform\State\Pagination; 15 | 16 | /** 17 | * Partial Paginator Interface. 18 | * 19 | * @author Baptiste Meyer 20 | * 21 | * @template T of object 22 | * 23 | * @extends \Traversable 24 | */ 25 | interface PartialPaginatorInterface extends \Traversable, \Countable 26 | { 27 | /** 28 | * Gets the current page number. 29 | */ 30 | public function getCurrentPage(): float; 31 | 32 | /** 33 | * Gets the number of items by page. 34 | */ 35 | public function getItemsPerPage(): float; 36 | } 37 | -------------------------------------------------------------------------------- /ProcessorInterface.php: -------------------------------------------------------------------------------- 1 | 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 ApiPlatform\State; 15 | 16 | use ApiPlatform\Metadata\Operation; 17 | 18 | /** 19 | * Process data: send an email, persist to storage, add to queue etc. 20 | * 21 | * @template T 22 | * 23 | * @author Antoine Bluchet 24 | */ 25 | interface ProcessorInterface 26 | { 27 | /** 28 | * Processes the state. 29 | * 30 | * @param array $uriVariables 31 | * @param array $context 32 | * 33 | * @return T 34 | */ 35 | public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []); 36 | } 37 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | ./Tests/ 18 | 19 | 20 | 21 | 22 | 23 | ./ 24 | 25 | 26 | ./Tests 27 | ./vendor 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /Pagination/PaginatorInterface.php: -------------------------------------------------------------------------------- 1 | 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 ApiPlatform\State\Pagination; 15 | 16 | /** 17 | * The \Countable implementation should return the number of items on the 18 | * current page, as an integer. 19 | * 20 | * @author Kévin Dunglas 21 | * 22 | * @template T of object 23 | * 24 | * @extends PartialPaginatorInterface 25 | */ 26 | interface PaginatorInterface extends PartialPaginatorInterface 27 | { 28 | /** 29 | * Gets last page. 30 | */ 31 | public function getLastPage(): float; 32 | 33 | /** 34 | * Gets the number of items in the whole collection. 35 | */ 36 | public function getTotalItems(): float; 37 | } 38 | -------------------------------------------------------------------------------- /ProviderInterface.php: -------------------------------------------------------------------------------- 1 | 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 ApiPlatform\State; 15 | 16 | use ApiPlatform\Metadata\Operation; 17 | 18 | /** 19 | * Retrieves data from a persistence layer. 20 | * 21 | * @template T of object 22 | * 23 | * @author Antoine Bluchet 24 | */ 25 | interface ProviderInterface 26 | { 27 | /** 28 | * Provides data. 29 | * 30 | * @param array $uriVariables 31 | * @param array $context 32 | * 33 | * @return T|Pagination\PartialPaginatorInterface|iterable|null 34 | */ 35 | public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null; 36 | } 37 | -------------------------------------------------------------------------------- /SerializerAwareProviderTrait.php: -------------------------------------------------------------------------------- 1 | 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 ApiPlatform\State; 15 | 16 | use Psr\Container\ContainerInterface; 17 | use Symfony\Component\Serializer\SerializerInterface; 18 | 19 | /** 20 | * Injects serializer in providers. 21 | * 22 | * @author Vincent Chalamon 23 | */ 24 | trait SerializerAwareProviderTrait 25 | { 26 | /** 27 | * @internal 28 | */ 29 | private ContainerInterface $serializerLocator; 30 | 31 | public function setSerializerLocator(ContainerInterface $serializerLocator): void 32 | { 33 | $this->serializerLocator = $serializerLocator; 34 | } 35 | 36 | private function getSerializer(): SerializerInterface 37 | { 38 | return $this->serializerLocator->get('serializer'); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT license 2 | 3 | Copyright (c) 2015-present Kévin Dunglas 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is furnished 10 | to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /ObjectProvider.php: -------------------------------------------------------------------------------- 1 | 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 ApiPlatform\State; 15 | 16 | use ApiPlatform\Exception\RuntimeException; 17 | use ApiPlatform\Metadata\Operation; 18 | 19 | /** 20 | * An ItemProvider that just creates a new object. 21 | * 22 | * @author Antoine Bluchet 23 | * 24 | * @experimental 25 | */ 26 | final class ObjectProvider implements ProviderInterface 27 | { 28 | public function provide(Operation $operation, array $uriVariables = [], array $context = []): ?object 29 | { 30 | try { 31 | return new ($operation->getClass()); 32 | } catch (\Throwable $e) { 33 | throw new RuntimeException(sprintf('An error occurred while trying to create an instance of the "%s" resource. Consider writing your own "%s" implementation and setting it as `provider` on your operation instead.', $operation->getClass(), ProviderInterface::class), 0, $e); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /CallableProcessor.php: -------------------------------------------------------------------------------- 1 | 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 ApiPlatform\State; 15 | 16 | use ApiPlatform\Exception\RuntimeException; 17 | use ApiPlatform\Metadata\Operation; 18 | use Psr\Container\ContainerInterface; 19 | 20 | final class CallableProcessor implements ProcessorInterface 21 | { 22 | public function __construct(private readonly ContainerInterface $locator) 23 | { 24 | } 25 | 26 | /** 27 | * {@inheritdoc} 28 | */ 29 | public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []) 30 | { 31 | if (!($processor = $operation->getProcessor())) { 32 | return null; 33 | } 34 | 35 | if (\is_callable($processor)) { 36 | return $processor($data, $operation, $uriVariables, $context); 37 | } 38 | 39 | if (!$this->locator->has($processor)) { 40 | throw new RuntimeException(sprintf('Processor "%s" not found on operation "%s"', $processor, $operation->getName())); 41 | } 42 | 43 | /** @var ProcessorInterface $processorInstance */ 44 | $processorInstance = $this->locator->get($processor); 45 | 46 | return $processorInstance->process($data, $operation, $uriVariables, $context); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /CallableProvider.php: -------------------------------------------------------------------------------- 1 | 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 ApiPlatform\State; 15 | 16 | use ApiPlatform\Metadata\Operation; 17 | use ApiPlatform\State\Exception\ProviderNotFoundException; 18 | use Psr\Container\ContainerInterface; 19 | 20 | final class CallableProvider implements ProviderInterface 21 | { 22 | public function __construct(private readonly ContainerInterface $locator) 23 | { 24 | } 25 | 26 | /** 27 | * {@inheritdoc} 28 | */ 29 | public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null 30 | { 31 | if (\is_callable($provider = $operation->getProvider())) { 32 | return $provider($operation, $uriVariables, $context); 33 | } 34 | 35 | if (\is_string($provider)) { 36 | if (!$this->locator->has($provider)) { 37 | throw new ProviderNotFoundException(sprintf('Provider "%s" not found on operation "%s"', $provider, $operation->getName())); 38 | } 39 | 40 | /** @var ProviderInterface $providerInstance */ 41 | $providerInstance = $this->locator->get($provider); 42 | 43 | return $providerInstance->provide($operation, $uriVariables, $context); 44 | } 45 | 46 | throw new ProviderNotFoundException(sprintf('Provider not found on operation "%s"', $operation->getName())); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "api-platform/state", 3 | "description": "API Platform state interfaces", 4 | "type": "library", 5 | "keywords": [ 6 | "REST", 7 | "GraphQL", 8 | "API", 9 | "JSON-LD", 10 | "Hydra", 11 | "JSONAPI", 12 | "OpenAPI", 13 | "HAL", 14 | "Swagger" 15 | ], 16 | "homepage": "https://api-platform.com", 17 | "license": "MIT", 18 | "authors": [ 19 | { 20 | "name": "Kévin Dunglas", 21 | "email": "kevin@dunglas.fr", 22 | "homepage": "https://dunglas.fr" 23 | }, 24 | { 25 | "name": "API Platform Community", 26 | "homepage": "https://api-platform.com/community/contributors" 27 | } 28 | ], 29 | "require": { 30 | "php": ">=8.1" 31 | }, 32 | "require-dev": { 33 | "phpspec/prophecy-phpunit": "^2.0", 34 | "symfony/phpunit-bridge": "^6.1", 35 | "sebastian/comparator": "<5.0" 36 | }, 37 | "autoload": { 38 | "psr-4": { 39 | "ApiPlatform\\State\\": "" 40 | }, 41 | "exclude-from-classmap": [ 42 | "/Tests/" 43 | ] 44 | }, 45 | "config": { 46 | "preferred-install": { 47 | "*": "dist" 48 | }, 49 | "sort-packages": true, 50 | "allow-plugins": { 51 | "composer/package-versions-deprecated": true, 52 | "phpstan/extension-installer": true 53 | } 54 | }, 55 | "extra": { 56 | "branch-alias": { 57 | "dev-main": "3.2.x-dev" 58 | }, 59 | "symfony": { 60 | "require": "^6.1" 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Pagination/TraversablePaginator.php: -------------------------------------------------------------------------------- 1 | 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 ApiPlatform\State\Pagination; 15 | 16 | final class TraversablePaginator implements \IteratorAggregate, PaginatorInterface 17 | { 18 | public function __construct(private readonly \Traversable $traversable, private readonly float $currentPage, private readonly float $itemsPerPage, private readonly float $totalItems) 19 | { 20 | } 21 | 22 | /** 23 | * {@inheritdoc} 24 | */ 25 | public function getCurrentPage(): float 26 | { 27 | return $this->currentPage; 28 | } 29 | 30 | /** 31 | * {@inheritdoc} 32 | */ 33 | public function getLastPage(): float 34 | { 35 | if (0. >= $this->itemsPerPage) { 36 | return 1.; 37 | } 38 | 39 | return max(ceil($this->totalItems / $this->itemsPerPage) ?: 1., 1.); 40 | } 41 | 42 | /** 43 | * {@inheritdoc} 44 | */ 45 | public function getItemsPerPage(): float 46 | { 47 | return $this->itemsPerPage; 48 | } 49 | 50 | /** 51 | * {@inheritdoc} 52 | */ 53 | public function getTotalItems(): float 54 | { 55 | return $this->totalItems; 56 | } 57 | 58 | /** 59 | * {@inheritdoc} 60 | */ 61 | public function count(): int 62 | { 63 | if ($this->getCurrentPage() < $this->getLastPage()) { 64 | return (int) ceil($this->itemsPerPage); 65 | } 66 | 67 | if (0. >= $this->itemsPerPage) { 68 | return (int) ceil($this->totalItems); 69 | } 70 | 71 | return $this->totalItems % $this->itemsPerPage; 72 | } 73 | 74 | /** 75 | * {@inheritdoc} 76 | */ 77 | public function getIterator(): \Traversable 78 | { 79 | return $this->traversable; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /Pagination/ArrayPaginator.php: -------------------------------------------------------------------------------- 1 | 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 ApiPlatform\State\Pagination; 15 | 16 | /** 17 | * Paginator for arrays. 18 | * 19 | * @author Alan Poulain 20 | */ 21 | final class ArrayPaginator implements \IteratorAggregate, PaginatorInterface 22 | { 23 | private \Traversable $iterator; 24 | private readonly int $firstResult; 25 | private readonly int $maxResults; 26 | private readonly int $totalItems; 27 | 28 | public function __construct(array $results, int $firstResult, int $maxResults) 29 | { 30 | if ($maxResults > 0) { 31 | $this->iterator = new \LimitIterator(new \ArrayIterator($results), $firstResult, $maxResults); 32 | } else { 33 | $this->iterator = new \EmptyIterator(); 34 | } 35 | $this->firstResult = $firstResult; 36 | $this->maxResults = $maxResults; 37 | $this->totalItems = \count($results); 38 | } 39 | 40 | /** 41 | * {@inheritdoc} 42 | */ 43 | public function getCurrentPage(): float 44 | { 45 | if (0 >= $this->maxResults) { 46 | return 1.; 47 | } 48 | 49 | return floor($this->firstResult / $this->maxResults) + 1.; 50 | } 51 | 52 | /** 53 | * {@inheritdoc} 54 | */ 55 | public function getLastPage(): float 56 | { 57 | if (0 >= $this->maxResults) { 58 | return 1.; 59 | } 60 | 61 | return ceil($this->totalItems / $this->maxResults) ?: 1.; 62 | } 63 | 64 | /** 65 | * {@inheritdoc} 66 | */ 67 | public function getItemsPerPage(): float 68 | { 69 | return (float) $this->maxResults; 70 | } 71 | 72 | /** 73 | * {@inheritdoc} 74 | */ 75 | public function getTotalItems(): float 76 | { 77 | return (float) $this->totalItems; 78 | } 79 | 80 | /** 81 | * {@inheritdoc} 82 | */ 83 | public function count(): int 84 | { 85 | return iterator_count($this->iterator); 86 | } 87 | 88 | /** 89 | * {@inheritdoc} 90 | */ 91 | public function getIterator(): \Traversable 92 | { 93 | return $this->iterator; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /Pagination/PaginationOptions.php: -------------------------------------------------------------------------------- 1 | 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 ApiPlatform\State\Pagination; 15 | 16 | final class PaginationOptions 17 | { 18 | public function __construct(private readonly bool $paginationEnabled = true, private readonly string $paginationPageParameterName = 'page', private readonly bool $clientItemsPerPage = false, private readonly string $itemsPerPageParameterName = 'itemsPerPage', private readonly bool $paginationClientEnabled = false, private readonly string $paginationClientEnabledParameterName = 'pagination', private readonly int $itemsPerPage = 30, private readonly ?int $maximumItemsPerPage = null, private readonly bool $partialPaginationEnabled = false, private readonly bool $clientPartialPaginationEnabled = false, private readonly string $partialPaginationParameterName = 'partial') 19 | { 20 | } 21 | 22 | public function isPaginationEnabled(): bool 23 | { 24 | return $this->paginationEnabled; 25 | } 26 | 27 | public function getPaginationPageParameterName(): string 28 | { 29 | return $this->paginationPageParameterName; 30 | } 31 | 32 | public function getClientItemsPerPage(): bool 33 | { 34 | return $this->clientItemsPerPage; 35 | } 36 | 37 | public function getItemsPerPageParameterName(): string 38 | { 39 | return $this->itemsPerPageParameterName; 40 | } 41 | 42 | public function getPaginationClientEnabled(): bool 43 | { 44 | return $this->paginationClientEnabled; 45 | } 46 | 47 | public function isPaginationClientEnabled(): bool 48 | { 49 | return $this->paginationClientEnabled; 50 | } 51 | 52 | public function getPaginationClientEnabledParameterName(): string 53 | { 54 | return $this->paginationClientEnabledParameterName; 55 | } 56 | 57 | public function getItemsPerPage(): int 58 | { 59 | return $this->itemsPerPage; 60 | } 61 | 62 | public function getMaximumItemsPerPage(): ?int 63 | { 64 | return $this->maximumItemsPerPage; 65 | } 66 | 67 | public function isPartialPaginationEnabled(): bool 68 | { 69 | return $this->partialPaginationEnabled; 70 | } 71 | 72 | public function isClientPartialPaginationEnabled(): bool 73 | { 74 | return $this->clientPartialPaginationEnabled; 75 | } 76 | 77 | public function getPartialPaginationParameterName(): string 78 | { 79 | return $this->partialPaginationParameterName; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /UriVariablesResolverTrait.php: -------------------------------------------------------------------------------- 1 | 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 ApiPlatform\State; 15 | 16 | use ApiPlatform\Api\CompositeIdentifierParser; 17 | use ApiPlatform\Api\UriVariablesConverterInterface; 18 | use ApiPlatform\Exception\InvalidIdentifierException; 19 | use ApiPlatform\Metadata\HttpOperation; 20 | 21 | trait UriVariablesResolverTrait 22 | { 23 | private ?UriVariablesConverterInterface $uriVariablesConverter = null; 24 | 25 | /** 26 | * Resolves an operation's UriVariables to their identifiers values. 27 | */ 28 | private function getOperationUriVariables(?HttpOperation $operation = null, array $parameters = [], ?string $resourceClass = null): array 29 | { 30 | $identifiers = []; 31 | 32 | if (!$operation) { 33 | return $identifiers; 34 | } 35 | 36 | $uriVariablesMap = []; 37 | foreach ($operation->getUriVariables() ?? [] as $parameterName => $uriVariableDefinition) { 38 | if (!isset($parameters[$parameterName])) { 39 | if (!isset($parameters['id'])) { 40 | throw new InvalidIdentifierException(sprintf('Parameter "%s" not found, check the identifiers configuration.', $parameterName)); 41 | } 42 | 43 | $parameterName = 'id'; 44 | } 45 | 46 | if (($uriVariableDefinition->getCompositeIdentifier() ?? true) && 1 < ($numIdentifiers = \count($uriVariableDefinition->getIdentifiers() ?? []))) { 47 | $currentIdentifiers = CompositeIdentifierParser::parse($parameters[$parameterName]); 48 | 49 | if (($foundNumIdentifiers = \count($currentIdentifiers)) !== $numIdentifiers) { 50 | throw new InvalidIdentifierException(sprintf('We expected "%s" identifiers and got "%s".', $numIdentifiers, $foundNumIdentifiers)); 51 | } 52 | 53 | foreach ($currentIdentifiers as $key => $value) { 54 | $identifiers[$key] = $value; 55 | $uriVariablesMap[$key] = $uriVariableDefinition; 56 | } 57 | 58 | continue; 59 | } 60 | 61 | $identifiers[$parameterName] = $parameters[$parameterName]; 62 | $uriVariablesMap[$parameterName] = $uriVariableDefinition; 63 | } 64 | 65 | if ($this->uriVariablesConverter) { 66 | $context = ['operation' => $operation, 'uri_variables_map' => $uriVariablesMap]; 67 | $identifiers = $this->uriVariablesConverter->convert($identifiers, $operation->getClass() ?? $resourceClass, $context); 68 | } 69 | 70 | return $identifiers; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /CreateProvider.php: -------------------------------------------------------------------------------- 1 | 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 ApiPlatform\State; 15 | 16 | use ApiPlatform\Exception\RuntimeException; 17 | use ApiPlatform\Metadata\Get; 18 | use ApiPlatform\Metadata\HttpOperation; 19 | use ApiPlatform\Metadata\Link; 20 | use ApiPlatform\Metadata\Operation; 21 | use ApiPlatform\Metadata\Post; 22 | use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; 23 | use Symfony\Component\PropertyAccess\PropertyAccess; 24 | use Symfony\Component\PropertyAccess\PropertyAccessorInterface; 25 | 26 | /** 27 | * An ItemProvider for POST operations on generated subresources. 28 | * 29 | * @see ApiPlatform\Tests\Fixtures\TestBundle\Entity\SubresourceEmployee 30 | * 31 | * @author Antoine Bluchet 32 | * 33 | * @experimental 34 | */ 35 | final class CreateProvider implements ProviderInterface 36 | { 37 | public function __construct(private ProviderInterface $decorated, private ?PropertyAccessorInterface $propertyAccessor = null) 38 | { 39 | $this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor(); 40 | } 41 | 42 | public function provide(Operation $operation, array $uriVariables = [], array $context = []): ?object 43 | { 44 | if (!$uriVariables || !$operation instanceof HttpOperation || null !== $operation->getController()) { 45 | return $this->decorated->provide($operation, $uriVariables, $context); 46 | } 47 | 48 | $operationUriVariables = $operation->getUriVariables(); 49 | $relationClass = current($operationUriVariables)->getFromClass(); 50 | $key = key($operationUriVariables); 51 | $relationUriVariables = []; 52 | 53 | foreach ($operationUriVariables as $parameterName => $value) { 54 | if ($key === $parameterName) { 55 | $relationUriVariables['id'] = new Link(identifiers: $value->getIdentifiers(), fromClass: $value->getFromClass(), parameterName: $key); 56 | continue; 57 | } 58 | 59 | $relationUriVariables[$parameterName] = $value; 60 | } 61 | 62 | $relation = $this->decorated->provide(new Get(uriVariables: $relationUriVariables, class: $relationClass), $uriVariables); 63 | if (!$relation) { 64 | throw new NotFoundHttpException('Not Found'); 65 | } 66 | 67 | try { 68 | $resource = new ($operation->getClass()); 69 | } catch (\Throwable $e) { 70 | throw new RuntimeException(sprintf('An error occurred while trying to create an instance of the "%s" resource. Consider writing your own "%s" implementation and setting it as `provider` on your operation instead.', $operation->getClass(), ProviderInterface::class), 0, $e); 71 | } 72 | $property = $operationUriVariables[$key]->getToProperty() ?? $key; 73 | $this->propertyAccessor->setValue($resource, $property, $relation); 74 | 75 | return $resource; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Pagination/Pagination.php: -------------------------------------------------------------------------------- 1 | 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 ApiPlatform\State\Pagination; 15 | 16 | use ApiPlatform\Exception\InvalidArgumentException; 17 | use ApiPlatform\Metadata\Operation; 18 | 19 | /** 20 | * Pagination configuration. 21 | * 22 | * @author Baptiste Meyer 23 | */ 24 | final class Pagination 25 | { 26 | private readonly array $options; 27 | private readonly array $graphQlOptions; 28 | 29 | public function __construct(array $options = [], array $graphQlOptions = []) 30 | { 31 | $this->options = array_merge([ 32 | 'enabled' => true, 33 | 'client_enabled' => false, 34 | 'client_items_per_page' => false, 35 | 'items_per_page' => 30, 36 | 'page_default' => 1, 37 | 'page_parameter_name' => 'page', 38 | 'enabled_parameter_name' => 'pagination', 39 | 'items_per_page_parameter_name' => 'itemsPerPage', 40 | 'maximum_items_per_page' => null, 41 | 'partial' => false, 42 | 'client_partial' => false, 43 | 'partial_parameter_name' => 'partial', 44 | ], $options); 45 | $this->graphQlOptions = array_merge([ 46 | 'enabled' => true, 47 | ], $graphQlOptions); 48 | } 49 | 50 | /** 51 | * Gets the current page. 52 | * 53 | * @throws InvalidArgumentException 54 | */ 55 | public function getPage(array $context = []): int 56 | { 57 | $page = (int) $this->getParameterFromContext( 58 | $context, 59 | $this->options['page_parameter_name'], 60 | $this->options['page_default'] 61 | ); 62 | 63 | if (1 > $page) { 64 | throw new InvalidArgumentException('Page should not be less than 1'); 65 | } 66 | 67 | return $page; 68 | } 69 | 70 | /** 71 | * Gets the current offset. 72 | */ 73 | public function getOffset(?Operation $operation = null, array $context = []): int 74 | { 75 | $graphql = (bool) ($context['graphql_operation_name'] ?? false); 76 | 77 | $limit = $this->getLimit($operation, $context); 78 | 79 | if ($graphql && null !== ($after = $this->getParameterFromContext($context, 'after'))) { 80 | return false === ($after = base64_decode($after, true)) ? 0 : (int) $after + 1; 81 | } 82 | 83 | if ($graphql && null !== ($before = $this->getParameterFromContext($context, 'before'))) { 84 | return ($offset = (false === ($before = base64_decode($before, true)) ? 0 : (int) $before - $limit)) < 0 ? 0 : $offset; 85 | } 86 | 87 | if ($graphql && null !== ($last = $this->getParameterFromContext($context, 'last'))) { 88 | return ($offset = ($context['count'] ?? 0) - $last) < 0 ? 0 : $offset; 89 | } 90 | 91 | $offset = ($this->getPage($context) - 1) * $limit; 92 | 93 | if (!\is_int($offset)) { 94 | throw new InvalidArgumentException('Page parameter is too large.'); 95 | } 96 | 97 | return $offset; 98 | } 99 | 100 | /** 101 | * Gets the current limit. 102 | * 103 | * @throws InvalidArgumentException 104 | */ 105 | public function getLimit(?Operation $operation = null, array $context = []): int 106 | { 107 | $graphql = (bool) ($context['graphql_operation_name'] ?? false); 108 | 109 | $limit = $operation?->getPaginationItemsPerPage() ?? $this->options['items_per_page']; 110 | $clientLimit = $operation?->getPaginationClientItemsPerPage() ?? $this->options['client_items_per_page']; 111 | 112 | if ($graphql && null !== ($first = $this->getParameterFromContext($context, 'first'))) { 113 | $limit = $first; 114 | } 115 | 116 | if ($graphql && null !== ($last = $this->getParameterFromContext($context, 'last'))) { 117 | $limit = $last; 118 | } 119 | 120 | if ($graphql && null !== ($before = $this->getParameterFromContext($context, 'before')) 121 | && (false === ($before = base64_decode($before, true)) ? 0 : (int) $before - $limit) < 0) { 122 | $limit = (int) $before; 123 | } 124 | 125 | if ($clientLimit) { 126 | $limit = (int) $this->getParameterFromContext($context, $this->options['items_per_page_parameter_name'], $limit); 127 | $maxItemsPerPage = $operation?->getPaginationMaximumItemsPerPage() ?? $this->options['maximum_items_per_page']; 128 | 129 | if (null !== $maxItemsPerPage && $limit > $maxItemsPerPage) { 130 | $limit = $maxItemsPerPage; 131 | } 132 | } 133 | 134 | if (0 > $limit) { 135 | throw new InvalidArgumentException('Limit should not be less than 0'); 136 | } 137 | 138 | return $limit; 139 | } 140 | 141 | /** 142 | * Gets info about the pagination. 143 | * 144 | * Returns an array with the following info as values: 145 | * - the page {@see Pagination::getPage} 146 | * - the offset {@see Pagination::getOffset} 147 | * - the limit {@see Pagination::getLimit} 148 | * 149 | * @throws InvalidArgumentException 150 | */ 151 | public function getPagination(?Operation $operation = null, array $context = []): array 152 | { 153 | $page = $this->getPage($context); 154 | $limit = $this->getLimit($operation, $context); 155 | 156 | if (0 === $limit && 1 < $page) { 157 | throw new InvalidArgumentException('Page should not be greater than 1 if limit is equal to 0'); 158 | } 159 | 160 | return [$page, $this->getOffset($operation, $context), $limit]; 161 | } 162 | 163 | /** 164 | * Is the pagination enabled? 165 | */ 166 | public function isEnabled(?Operation $operation = null, array $context = []): bool 167 | { 168 | return $this->getEnabled($context, $operation); 169 | } 170 | 171 | /** 172 | * Is the pagination enabled for GraphQL? 173 | */ 174 | public function isGraphQlEnabled(?Operation $operation = null, array $context = []): bool 175 | { 176 | return $this->getGraphQlEnabled($operation); 177 | } 178 | 179 | /** 180 | * Is the partial pagination enabled? 181 | */ 182 | public function isPartialEnabled(?Operation $operation = null, array $context = []): bool 183 | { 184 | return $this->getEnabled($context, $operation, true); 185 | } 186 | 187 | public function getOptions(): array 188 | { 189 | return $this->options; 190 | } 191 | 192 | public function getGraphQlPaginationType(Operation $operation): string 193 | { 194 | return $operation->getPaginationType() ?? 'cursor'; 195 | } 196 | 197 | /** 198 | * Is the classic or partial pagination enabled? 199 | */ 200 | private function getEnabled(array $context, ?Operation $operation = null, bool $partial = false): bool 201 | { 202 | $enabled = $this->options[$partial ? 'partial' : 'enabled']; 203 | $clientEnabled = $this->options[$partial ? 'client_partial' : 'client_enabled']; 204 | 205 | $enabled = ($partial ? $operation?->getPaginationPartial() : $operation?->getPaginationEnabled()) ?? $enabled; 206 | $clientEnabled = ($partial ? $operation?->getPaginationClientPartial() : $operation?->getPaginationClientEnabled()) ?? $clientEnabled; 207 | 208 | if ($clientEnabled) { 209 | return filter_var($this->getParameterFromContext($context, $this->options[$partial ? 'partial_parameter_name' : 'enabled_parameter_name'], $enabled), \FILTER_VALIDATE_BOOLEAN); 210 | } 211 | 212 | return (bool) $enabled; 213 | } 214 | 215 | private function getGraphQlEnabled(?Operation $operation): bool 216 | { 217 | $enabled = $this->graphQlOptions['enabled']; 218 | 219 | return $operation?->getPaginationEnabled() ?? $enabled; 220 | } 221 | 222 | /** 223 | * Gets the given pagination parameter name from the given context. 224 | */ 225 | private function getParameterFromContext(array $context, string $parameterName, mixed $default = null) 226 | { 227 | $filters = $context['filters'] ?? []; 228 | 229 | return \array_key_exists($parameterName, $filters) ? $filters[$parameterName] : $default; 230 | } 231 | } 232 | --------------------------------------------------------------------------------