diff --git a/composer.json b/composer.json index 7ea9312..1492c97 100644 --- a/composer.json +++ b/composer.json @@ -21,11 +21,15 @@ } }, "require": { - "symfony/serializer": "^5.2" + "php": ">=7.4", + "symfony/serializer": "^5.2|^6.0" }, "extra": { "laminas": { "config-provider": "Zfegg\\ApiSerializerExt\\ConfigProvider" } + }, + "require-dev": { + "phpunit/phpunit": "^9.5" } -} \ No newline at end of file +} diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 4c66f07..b30a4a7 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,17 +1,13 @@ - - - - ./test - - - - - - ./src - - + + + + ./src + + + + + ./test + + diff --git a/src/Basic/ArrayNormalizer.php b/src/Basic/ArrayNormalizer.php index 6ca85c4..f5a0bda 100644 --- a/src/Basic/ArrayNormalizer.php +++ b/src/Basic/ArrayNormalizer.php @@ -24,10 +24,7 @@ final class ArrayNormalizer implements NormalizerInterface, SerializerAwareInter AbstractNormalizer::IGNORED_ATTRIBUTES => [], ]; - /** - * @var NameConverterInterface|null - */ - private $nameConverter; + private ?NameConverterInterface $nameConverter; public function __construct(NameConverterInterface $nameConverter = null, array $defaultContext = []) { diff --git a/src/Basic/CollectionNormalizer.php b/src/Basic/CollectionNormalizer.php index 786b3ce..f3099ce 100644 --- a/src/Basic/CollectionNormalizer.php +++ b/src/Basic/CollectionNormalizer.php @@ -1,14 +1,5 @@ - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - declare(strict_types=1); namespace Zfegg\ApiSerializerExt\Basic; @@ -16,13 +7,6 @@ use Zfegg\ApiSerializerExt\Serializer\AbstractCollectionNormalizer; use Symfony\Component\Serializer\Exception\UnexpectedValueException; -/** - * Normalizes collections in the JSON API format. - * - * @author Kevin Dunglas - * @author Hamza Amrouche - * @author Baptiste Meyer - */ final class CollectionNormalizer extends AbstractCollectionNormalizer { public const FORMAT = 'json'; diff --git a/src/Basic/ItemNormalizer.php b/src/Basic/ItemNormalizer.php deleted file mode 100644 index 7b5a34b..0000000 --- a/src/Basic/ItemNormalizer.php +++ /dev/null @@ -1,42 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace Zfegg\ApiSerializerExt\Basic; - -use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer; - -/** - * Converts between objects and array. - * - * @author Kévin Dunglas - * @author Amrouche Hamza - * @author Baptiste Meyer - */ -final class ItemNormalizer extends AbstractObjectNormalizer -{ - protected function extractAttributes(object $object, string $format = null, array $context = []) - { - // TODO: Implement extractAttributes() method. - } - - protected function getAttributeValue(object $object, string $attribute, string $format = null, array $context = []) - { - // TODO: Implement getAttributeValue() method. - } - - protected function setAttributeValue(object $object, string $attribute, $value, string $format = null, array $context = []) - { - // TODO: Implement setAttributeValue() method. - } -} - diff --git a/src/Hal/EntrypointNormalizer.php b/src/Hal/EntrypointNormalizer.php deleted file mode 100644 index 6364202..0000000 --- a/src/Hal/EntrypointNormalizer.php +++ /dev/null @@ -1,82 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace Zfegg\ApiSerializerExt\Hal; - -use Zfegg\ApiSerializerExt\Api\Entrypoint; -use Zfegg\ApiSerializerExt\Api\IriConverterInterface; -use Zfegg\ApiSerializerExt\Api\UrlGeneratorInterface; -use Zfegg\ApiSerializerExt\Exception\InvalidArgumentException; -use Zfegg\ApiSerializerExt\Metadata\Resource\Factory\ResourceMetadataFactoryInterface; -use Symfony\Component\Serializer\Normalizer\CacheableSupportsMethodInterface; -use Symfony\Component\Serializer\Normalizer\NormalizerInterface; - -/** - * Normalizes the API entrypoint. - * - * @author Kévin Dunglas - */ -final class EntrypointNormalizer implements NormalizerInterface, CacheableSupportsMethodInterface -{ - public const FORMAT = 'jsonhal'; - - private $resourceMetadataFactory; - private $iriConverter; - private $urlGenerator; - - public function __construct(ResourceMetadataFactoryInterface $resourceMetadataFactory, IriConverterInterface $iriConverter, UrlGeneratorInterface $urlGenerator) - { - $this->resourceMetadataFactory = $resourceMetadataFactory; - $this->iriConverter = $iriConverter; - $this->urlGenerator = $urlGenerator; - } - - /** - * {@inheritdoc} - */ - public function normalize($object, $format = null, array $context = []) - { - $entrypoint = ['_links' => ['self' => ['href' => $this->urlGenerator->generate('api_entrypoint')]]]; - - foreach ($object->getResourceNameCollection() as $resourceClass) { - $resourceMetadata = $this->resourceMetadataFactory->create($resourceClass); - - if (empty($resourceMetadata->getCollectionOperations())) { - continue; - } - try { - $entrypoint['_links'][lcfirst($resourceMetadata->getShortName())]['href'] = $this->iriConverter->getIriFromResourceClass($resourceClass); - } catch (InvalidArgumentException $ex) { - // Ignore resources without GET operations - } - } - - return $entrypoint; - } - - /** - * {@inheritdoc} - */ - public function supportsNormalization($data, $format = null) - { - return self::FORMAT === $format && $data instanceof Entrypoint; - } - - /** - * {@inheritdoc} - */ - public function hasCacheableSupportsMethod(): bool - { - return true; - } -} diff --git a/src/Hal/ItemNormalizer.php b/src/Hal/ItemNormalizer.php index 3ea8c38..b213259 100644 --- a/src/Hal/ItemNormalizer.php +++ b/src/Hal/ItemNormalizer.php @@ -14,8 +14,6 @@ namespace Zfegg\ApiSerializerExt\Hal; use Zfegg\ApiSerializerExt\Serializer\AbstractItemNormalizer; -use Zfegg\ApiSerializerExt\Serializer\CacheKeyTrait; -use Zfegg\ApiSerializerExt\Serializer\ContextTrait; use Zfegg\ApiSerializerExt\Util\ClassInfoTrait; use Symfony\Component\Serializer\Exception\LogicException; use Symfony\Component\Serializer\Exception\UnexpectedValueException; diff --git a/src/Hal/ObjectNormalizer.php b/src/Hal/ObjectNormalizer.php index b690d6a..71c6b72 100644 --- a/src/Hal/ObjectNormalizer.php +++ b/src/Hal/ObjectNormalizer.php @@ -13,7 +13,6 @@ namespace Zfegg\ApiSerializerExt\Hal; -use Zfegg\ApiSerializerExt\Api\IriConverterInterface; use Symfony\Component\Serializer\Exception\LogicException; use Symfony\Component\Serializer\Normalizer\CacheableSupportsMethodInterface; use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; @@ -30,7 +29,7 @@ final class ObjectNormalizer implements NormalizerInterface, DenormalizerInterfa private $decorated; private $iriConverter; - public function __construct(NormalizerInterface $decorated, IriConverterInterface $iriConverter) + public function __construct(NormalizerInterface $decorated, $iriConverter) { $this->decorated = $decorated; $this->iriConverter = $iriConverter; diff --git a/src/Hydra/CollectionFiltersNormalizer.php b/src/Hydra/CollectionFiltersNormalizer.php deleted file mode 100644 index f08440b..0000000 --- a/src/Hydra/CollectionFiltersNormalizer.php +++ /dev/null @@ -1,156 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace Zfegg\ApiSerializerExt\Hydra\Serializer; - -use Psr\Container\ContainerInterface; -use Zfegg\ApiSerializerExt\Api\FilterCollection; -use Zfegg\ApiSerializerExt\Api\FilterInterface; -use Zfegg\ApiSerializerExt\Api\FilterLocatorTrait; -use Zfegg\ApiSerializerExt\Api\ResourceClassResolverInterface; -use Zfegg\ApiSerializerExt\Metadata\Resource\Factory\ResourceMetadataFactoryInterface; -use Symfony\Component\Serializer\Exception\UnexpectedValueException; -use Symfony\Component\Serializer\Normalizer\CacheableSupportsMethodInterface; -use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface; -use Symfony\Component\Serializer\Normalizer\NormalizerInterface; - -/** - * Enhances the result of collection by adding the filters applied on collection. - * - * @author Samuel ROZE - */ -final class CollectionFiltersNormalizer implements NormalizerInterface, NormalizerAwareInterface, CacheableSupportsMethodInterface -{ - use FilterLocatorTrait; - - private $collectionNormalizer; - private $resourceMetadataFactory; - private $resourceClassResolver; - - /** - * @param ContainerInterface|FilterCollection $filterLocator The new filter locator or the deprecated filter collection - */ - public function __construct(NormalizerInterface $collectionNormalizer, - ResourceMetadataFactoryInterface $resourceMetadataFactory, - ResourceClassResolverInterface $resourceClassResolver, - $filterLocator) - { - $this->setFilterLocator($filterLocator); - - $this->collectionNormalizer = $collectionNormalizer; - $this->resourceMetadataFactory = $resourceMetadataFactory; - $this->resourceClassResolver = $resourceClassResolver; - } - - /** - * {@inheritdoc} - */ - public function supportsNormalization($data, $format = null) - { - return $this->collectionNormalizer->supportsNormalization($data, $format); - } - - /** - * {@inheritdoc} - */ - public function hasCacheableSupportsMethod(): bool - { - return $this->collectionNormalizer instanceof CacheableSupportsMethodInterface && $this->collectionNormalizer->hasCacheableSupportsMethod(); - } - - /** - * {@inheritdoc} - */ - public function normalize($object, $format = null, array $context = []) - { - $data = $this->collectionNormalizer->normalize($object, $format, $context); - if (!\is_array($data)) { - throw new UnexpectedValueException('Expected data to be an array'); - } - - if (!isset($context['resource_class']) || isset($context['api_sub_level'])) { - return $data; - } - - $resourceClass = $this->resourceClassResolver->getResourceClass($object, $context['resource_class']); - $resourceMetadata = $this->resourceMetadataFactory->create($resourceClass); - - $operationName = $context['collection_operation_name'] ?? null; - if (null === $operationName) { - $resourceFilters = $resourceMetadata->getAttribute('filters', []); - } else { - $resourceFilters = $resourceMetadata->getCollectionOperationAttribute($operationName, 'filters', [], true); - } - - if (!$resourceFilters) { - return $data; - } - - $requestParts = parse_url($context['request_uri'] ?? ''); - if (!\is_array($requestParts)) { - return $data; - } - - $currentFilters = []; - foreach ($resourceFilters as $filterId) { - if ($filter = $this->getFilter($filterId)) { - $currentFilters[] = $filter; - } - } - - if ($currentFilters) { - $data['hydra:search'] = $this->getSearch($resourceClass, $requestParts, $currentFilters); - } - - return $data; - } - - /** - * {@inheritdoc} - */ - public function setNormalizer(NormalizerInterface $normalizer) - { - if ($this->collectionNormalizer instanceof NormalizerAwareInterface) { - $this->collectionNormalizer->setNormalizer($normalizer); - } - } - - /** - * Returns the content of the Hydra search property. - * - * @param FilterInterface[] $filters - */ - private function getSearch(string $resourceClass, array $parts, array $filters): array - { - $variables = []; - $mapping = []; - foreach ($filters as $filter) { - foreach ($filter->getDescription($resourceClass) as $variable => $data) { - $variables[] = $variable; - $mapping[] = [ - '@type' => 'IriTemplateMapping', - 'variable' => $variable, - 'property' => $data['property'], - 'required' => $data['required'], - ]; - } - } - - return [ - '@type' => 'hydra:IriTemplate', - 'hydra:template' => sprintf('%s{?%s}', $parts['path'], implode(',', $variables)), - 'hydra:variableRepresentation' => 'BasicRepresentation', - 'hydra:mapping' => $mapping, - ]; - } -} diff --git a/src/Hydra/CollectionNormalizer.php b/src/Hydra/CollectionNormalizer.php deleted file mode 100644 index 7d46738..0000000 --- a/src/Hydra/CollectionNormalizer.php +++ /dev/null @@ -1,88 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace Zfegg\ApiSerializerExt\Hydra; - -use Zfegg\ApiSerializerExt\JsonLd\JsonLdContextTrait; -use Symfony\Component\Serializer\Normalizer\CacheableSupportsMethodInterface; -use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface; -use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait; -use Symfony\Component\Serializer\Normalizer\NormalizerInterface; - -/** - * This normalizer handles collections. - * - * @author Kevin Dunglas - * @author Samuel ROZE - */ -final class CollectionNormalizer implements NormalizerInterface, NormalizerAwareInterface, CacheableSupportsMethodInterface -{ - use JsonLdContextTrait; - use NormalizerAwareTrait; - - public const FORMAT = 'jsonld'; - - private $contextBuilder; - private $resourceClassResolver; - private $iriConverter; - - /** - * {@inheritdoc} - */ - public function supportsNormalization($data, $format = null, array $context = []) - { - return self::FORMAT === $format && is_iterable($data) && ! isset($context['api_sub_level']); - } - - /** - * {@inheritdoc} - * - * @param iterable $object - */ - public function normalize($object, $format = null, array $context = []) - { - if (isset($context['api_sub_level'])) { - return $this->normalizeRawCollection($object, $format, $context); - } - - $data['@type'] = 'hydra:Collection'; - - $data['hydra:member'] = []; - foreach ($object as $obj) { - $data['hydra:member'][] = $this->normalizer->normalize($obj, $format, $context); - } - - return $data; - } - - /** - * {@inheritdoc} - */ - public function hasCacheableSupportsMethod(): bool - { - return true; - } - - /** - * Normalizes a raw collection (not API resources). - */ - private function normalizeRawCollection(iterable $object, ?string $format, array $context): array - { - $data = []; - foreach ($object as $index => $obj) { - $data[$index] = $this->normalizer->normalize($obj, $format, $context); - } - - return $data; - } -} diff --git a/src/Hydra/ConstraintViolationListNormalizer.php b/src/Hydra/ConstraintViolationListNormalizer.php deleted file mode 100644 index c769ad9..0000000 --- a/src/Hydra/ConstraintViolationListNormalizer.php +++ /dev/null @@ -1,53 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace Zfegg\ApiSerializerExt\Hydra\Serializer; - -use Zfegg\ApiSerializerExt\Api\UrlGeneratorInterface; -use Zfegg\ApiSerializerExt\Serializer\AbstractConstraintViolationListNormalizer; -use Symfony\Component\Serializer\NameConverter\NameConverterInterface; - -/** - * Converts {@see \Symfony\Component\Validator\ConstraintViolationListInterface} to a Hydra error representation. - * - * @author Kévin Dunglas - */ -final class ConstraintViolationListNormalizer extends AbstractConstraintViolationListNormalizer -{ - public const FORMAT = 'jsonld'; - - private $urlGenerator; - - public function __construct(UrlGeneratorInterface $urlGenerator, array $serializePayloadFields = null, NameConverterInterface $nameConverter = null) - { - parent::__construct($serializePayloadFields, $nameConverter); - - $this->urlGenerator = $urlGenerator; - } - - /** - * {@inheritdoc} - */ - public function normalize($object, $format = null, array $context = []) - { - [$messages, $violations] = $this->getMessagesAndViolations($object); - - return [ - '@context' => $this->urlGenerator->generate('api_jsonld_context', ['shortName' => 'ConstraintViolationList']), - '@type' => 'ConstraintViolationList', - 'hydra:title' => $context['title'] ?? 'An error occurred', - 'hydra:description' => $messages ? implode("\n", $messages) : (string) $object, - 'violations' => $violations, - ]; - } -} diff --git a/src/Hydra/DocumentationNormalizer.php b/src/Hydra/DocumentationNormalizer.php deleted file mode 100644 index 46674a4..0000000 --- a/src/Hydra/DocumentationNormalizer.php +++ /dev/null @@ -1,564 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace Zfegg\ApiSerializerExt\Hydra\Serializer; - -use Zfegg\ApiSerializerExt\Api\OperationMethodResolverInterface; -use Zfegg\ApiSerializerExt\Api\OperationType; -use Zfegg\ApiSerializerExt\Api\ResourceClassResolverInterface; -use Zfegg\ApiSerializerExt\Api\UrlGeneratorInterface; -use Zfegg\ApiSerializerExt\Documentation\Documentation; -use Zfegg\ApiSerializerExt\JsonLd\ContextBuilderInterface; -use Zfegg\ApiSerializerExt\Metadata\Property\Factory\PropertyMetadataFactoryInterface; -use Zfegg\ApiSerializerExt\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; -use Zfegg\ApiSerializerExt\Metadata\Property\PropertyMetadata; -use Zfegg\ApiSerializerExt\Metadata\Property\SubresourceMetadata; -use Zfegg\ApiSerializerExt\Metadata\Resource\Factory\ResourceMetadataFactoryInterface; -use Zfegg\ApiSerializerExt\Metadata\Resource\ResourceMetadata; -use Zfegg\ApiSerializerExt\Operation\Factory\SubresourceOperationFactoryInterface; -use Symfony\Component\PropertyInfo\Type; -use Symfony\Component\Serializer\NameConverter\NameConverterInterface; -use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; -use Symfony\Component\Serializer\Normalizer\CacheableSupportsMethodInterface; -use Symfony\Component\Serializer\Normalizer\NormalizerInterface; - -/** - * Creates a machine readable Hydra API documentation. - * - * @author Kévin Dunglas - */ -final class DocumentationNormalizer implements NormalizerInterface, CacheableSupportsMethodInterface -{ - public const FORMAT = 'jsonld'; - - private $resourceMetadataFactory; - private $propertyNameCollectionFactory; - private $propertyMetadataFactory; - private $resourceClassResolver; - private $operationMethodResolver; - private $urlGenerator; - private $subresourceOperationFactory; - private $nameConverter; - - public function __construct(ResourceMetadataFactoryInterface $resourceMetadataFactory, PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, ResourceClassResolverInterface $resourceClassResolver, OperationMethodResolverInterface $operationMethodResolver = null, UrlGeneratorInterface $urlGenerator, SubresourceOperationFactoryInterface $subresourceOperationFactory = null, NameConverterInterface $nameConverter = null) - { - if ($operationMethodResolver) { - @trigger_error(sprintf('Passing an instance of %s to %s() is deprecated since version 2.5 and will be removed in 3.0.', OperationMethodResolverInterface::class, __METHOD__), E_USER_DEPRECATED); - } - - $this->resourceMetadataFactory = $resourceMetadataFactory; - $this->propertyNameCollectionFactory = $propertyNameCollectionFactory; - $this->propertyMetadataFactory = $propertyMetadataFactory; - $this->resourceClassResolver = $resourceClassResolver; - $this->operationMethodResolver = $operationMethodResolver; - $this->urlGenerator = $urlGenerator; - $this->subresourceOperationFactory = $subresourceOperationFactory; - $this->nameConverter = $nameConverter; - } - - /** - * {@inheritdoc} - */ - public function normalize($object, $format = null, array $context = []) - { - $classes = []; - $entrypointProperties = []; - - foreach ($object->getResourceNameCollection() as $resourceClass) { - $resourceMetadata = $this->resourceMetadataFactory->create($resourceClass); - $shortName = $resourceMetadata->getShortName(); - $prefixedShortName = $resourceMetadata->getIri() ?? "#$shortName"; - - $this->populateEntrypointProperties($resourceClass, $resourceMetadata, $shortName, $prefixedShortName, $entrypointProperties); - $classes[] = $this->getClass($resourceClass, $resourceMetadata, $shortName, $prefixedShortName, $context); - } - - return $this->computeDoc($object, $this->getClasses($entrypointProperties, $classes)); - } - - /** - * Populates entrypoint properties. - */ - private function populateEntrypointProperties(string $resourceClass, ResourceMetadata $resourceMetadata, string $shortName, string $prefixedShortName, array &$entrypointProperties) - { - $hydraCollectionOperations = $this->getHydraOperations($resourceClass, $resourceMetadata, $prefixedShortName, true); - if (empty($hydraCollectionOperations)) { - return; - } - - $entrypointProperty = [ - '@type' => 'hydra:SupportedProperty', - 'hydra:property' => [ - '@id' => sprintf('#Entrypoint/%s', lcfirst($shortName)), - '@type' => 'hydra:Link', - 'domain' => '#Entrypoint', - 'rdfs:label' => "The collection of $shortName resources", - 'rdfs:range' => [ - ['@id' => 'hydra:Collection'], - [ - 'owl:equivalentClass' => [ - 'owl:onProperty' => ['@id' => 'hydra:member'], - 'owl:allValuesFrom' => ['@id' => $prefixedShortName], - ], - ], - ], - 'hydra:supportedOperation' => $hydraCollectionOperations, - ], - 'hydra:title' => "The collection of $shortName resources", - 'hydra:readable' => true, - 'hydra:writable' => false, - ]; - - if ($resourceMetadata->getCollectionOperationAttribute('GET', 'deprecation_reason', null, true)) { - $entrypointProperty['owl:deprecated'] = true; - } - - $entrypointProperties[] = $entrypointProperty; - } - - /** - * Gets a Hydra class. - */ - private function getClass(string $resourceClass, ResourceMetadata $resourceMetadata, string $shortName, string $prefixedShortName, array $context): array - { - $class = [ - '@id' => $prefixedShortName, - '@type' => 'hydra:Class', - 'rdfs:label' => $shortName, - 'hydra:title' => $shortName, - 'hydra:supportedProperty' => $this->getHydraProperties($resourceClass, $resourceMetadata, $shortName, $prefixedShortName, $context), - 'hydra:supportedOperation' => $this->getHydraOperations($resourceClass, $resourceMetadata, $prefixedShortName, false), - ]; - - if (null !== $description = $resourceMetadata->getDescription()) { - $class['hydra:description'] = $description; - } - - if ($resourceMetadata->getAttribute('deprecation_reason')) { - $class['owl:deprecated'] = true; - } - - return $class; - } - - /** - * Gets the context for the property name factory. - */ - private function getPropertyNameCollectionFactoryContext(ResourceMetadata $resourceMetadata): array - { - $attributes = $resourceMetadata->getAttributes(); - $context = []; - - if (isset($attributes['normalization_context'][AbstractNormalizer::GROUPS])) { - $context['serializer_groups'] = (array) $attributes['normalization_context'][AbstractNormalizer::GROUPS]; - } - - if (!isset($attributes['denormalization_context'][AbstractNormalizer::GROUPS])) { - return $context; - } - - if (isset($context['serializer_groups'])) { - foreach ((array) $attributes['denormalization_context'][AbstractNormalizer::GROUPS] as $groupName) { - $context['serializer_groups'][] = $groupName; - } - - return $context; - } - - $context['serializer_groups'] = (array) $attributes['denormalization_context'][AbstractNormalizer::GROUPS]; - - return $context; - } - - /** - * Gets Hydra properties. - */ - private function getHydraProperties(string $resourceClass, ResourceMetadata $resourceMetadata, string $shortName, string $prefixedShortName, array $context): array - { - $classes = []; - foreach ($resourceMetadata->getCollectionOperations() as $operationName => $operation) { - $inputMetadata = $resourceMetadata->getTypedOperationAttribute(OperationType::COLLECTION, $operationName, 'input', ['class' => $resourceClass], true); - if (null !== $inputClass = $inputMetadata['class'] ?? null) { - $classes[$inputClass] = true; - } - - $outputMetadata = $resourceMetadata->getTypedOperationAttribute(OperationType::COLLECTION, $operationName, 'output', ['class' => $resourceClass], true); - if (null !== $outputClass = $outputMetadata['class'] ?? null) { - $classes[$outputClass] = true; - } - } - - /** @var string[] $classes */ - $classes = array_keys($classes); - $properties = []; - foreach ($classes as $class) { - foreach ($this->propertyNameCollectionFactory->create($class, $this->getPropertyNameCollectionFactoryContext($resourceMetadata)) as $propertyName) { - $propertyMetadata = $this->propertyMetadataFactory->create($class, $propertyName); - if (true === $propertyMetadata->isIdentifier() && false === $propertyMetadata->isWritable()) { - continue; - } - - if ($this->nameConverter) { - $propertyName = $this->nameConverter->normalize($propertyName, $class, self::FORMAT, $context); - } - - $properties[] = $this->getProperty($propertyMetadata, $propertyName, $prefixedShortName, $shortName); - } - } - - return $properties; - } - - /** - * Gets Hydra operations. - */ - private function getHydraOperations(string $resourceClass, ResourceMetadata $resourceMetadata, string $prefixedShortName, bool $collection): array - { - if (null === $operations = $collection ? $resourceMetadata->getCollectionOperations() : $resourceMetadata->getItemOperations()) { - return []; - } - - $hydraOperations = []; - foreach ($operations as $operationName => $operation) { - $hydraOperations[] = $this->getHydraOperation($resourceClass, $resourceMetadata, $operationName, $operation, $prefixedShortName, $collection ? OperationType::COLLECTION : OperationType::ITEM); - } - - if (null !== $this->subresourceOperationFactory) { - foreach ($this->subresourceOperationFactory->create($resourceClass) as $operationId => $operation) { - $subresourceMetadata = $this->resourceMetadataFactory->create($operation['resource_class']); - $propertyMetadata = $this->propertyMetadataFactory->create(end($operation['identifiers'])[1], $operation['property']); - $hydraOperations[] = $this->getHydraOperation($resourceClass, $subresourceMetadata, $operation['route_name'], $operation, "#{$subresourceMetadata->getShortName()}", OperationType::SUBRESOURCE, $propertyMetadata->getSubresource()); - } - } - - return $hydraOperations; - } - - /** - * Gets and populates if applicable a Hydra operation. - * - * @param SubresourceMetadata $subresourceMetadata - */ - private function getHydraOperation(string $resourceClass, ResourceMetadata $resourceMetadata, string $operationName, array $operation, string $prefixedShortName, string $operationType, SubresourceMetadata $subresourceMetadata = null): array - { - if ($this->operationMethodResolver) { - if (OperationType::COLLECTION === $operationType) { - $method = $this->operationMethodResolver->getCollectionOperationMethod($resourceClass, $operationName); - } elseif (OperationType::ITEM === $operationType) { - $method = $this->operationMethodResolver->getItemOperationMethod($resourceClass, $operationName); - } else { - $method = 'GET'; - } - } else { - $method = $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'method', 'GET'); - } - - $hydraOperation = $operation['hydra_context'] ?? []; - if ($resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'deprecation_reason', null, true)) { - $hydraOperation['owl:deprecated'] = true; - } - - $shortName = $resourceMetadata->getShortName(); - $inputMetadata = $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'input', ['class' => false]); - $inputClass = \array_key_exists('class', $inputMetadata) ? $inputMetadata['class'] : false; - $outputMetadata = $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'output', ['class' => false]); - $outputClass = \array_key_exists('class', $outputMetadata) ? $outputMetadata['class'] : false; - - if ('GET' === $method && OperationType::COLLECTION === $operationType) { - $hydraOperation += [ - '@type' => ['hydra:Operation', 'schema:FindAction'], - 'hydra:title' => "Retrieves the collection of $shortName resources.", - 'returns' => 'hydra:Collection', - ]; - } elseif ('GET' === $method && OperationType::SUBRESOURCE === $operationType) { - $hydraOperation += [ - '@type' => ['hydra:Operation', 'schema:FindAction'], - 'hydra:title' => $subresourceMetadata && $subresourceMetadata->isCollection() ? "Retrieves the collection of $shortName resources." : "Retrieves a $shortName resource.", - 'returns' => null === $outputClass ? 'owl:Nothing' : "#$shortName", - ]; - } elseif ('GET' === $method) { - $hydraOperation += [ - '@type' => ['hydra:Operation', 'schema:FindAction'], - 'hydra:title' => "Retrieves $shortName resource.", - 'returns' => null === $outputClass ? 'owl:Nothing' : $prefixedShortName, - ]; - } elseif ('PATCH' === $method) { - $hydraOperation += [ - '@type' => 'hydra:Operation', - 'hydra:title' => "Updates the $shortName resource.", - 'returns' => null === $outputClass ? 'owl:Nothing' : $prefixedShortName, - 'expects' => null === $inputClass ? 'owl:Nothing' : $prefixedShortName, - ]; - } elseif ('POST' === $method) { - $hydraOperation += [ - '@type' => ['hydra:Operation', 'schema:CreateAction'], - 'hydra:title' => "Creates a $shortName resource.", - 'returns' => null === $outputClass ? 'owl:Nothing' : $prefixedShortName, - 'expects' => null === $inputClass ? 'owl:Nothing' : $prefixedShortName, - ]; - } elseif ('PUT' === $method) { - $hydraOperation += [ - '@type' => ['hydra:Operation', 'schema:ReplaceAction'], - 'hydra:title' => "Replaces the $shortName resource.", - 'returns' => null === $outputClass ? 'owl:Nothing' : $prefixedShortName, - 'expects' => null === $inputClass ? 'owl:Nothing' : $prefixedShortName, - ]; - } elseif ('DELETE' === $method) { - $hydraOperation += [ - '@type' => ['hydra:Operation', 'schema:DeleteAction'], - 'hydra:title' => "Deletes the $shortName resource.", - 'returns' => 'owl:Nothing', - ]; - } - - $hydraOperation['hydra:method'] ?? $hydraOperation['hydra:method'] = $method; - - if (!isset($hydraOperation['rdfs:label']) && isset($hydraOperation['hydra:title'])) { - $hydraOperation['rdfs:label'] = $hydraOperation['hydra:title']; - } - - ksort($hydraOperation); - - return $hydraOperation; - } - - /** - * Gets the range of the property. - */ - private function getRange(PropertyMetadata $propertyMetadata): ?string - { - $jsonldContext = $propertyMetadata->getAttributes()['jsonld_context'] ?? []; - - if (isset($jsonldContext['@type'])) { - return $jsonldContext['@type']; - } - - if (null === $type = $propertyMetadata->getType()) { - return null; - } - - if ($type->isCollection() && null !== $collectionType = $type->getCollectionValueType()) { - $type = $collectionType; - } - - switch ($type->getBuiltinType()) { - case Type::BUILTIN_TYPE_STRING: - return 'xmls:string'; - case Type::BUILTIN_TYPE_INT: - return 'xmls:integer'; - case Type::BUILTIN_TYPE_FLOAT: - return 'xmls:decimal'; - case Type::BUILTIN_TYPE_BOOL: - return 'xmls:boolean'; - case Type::BUILTIN_TYPE_OBJECT: - if (null === $className = $type->getClassName()) { - return null; - } - - if (is_a($className, \DateTimeInterface::class, true)) { - return 'xmls:dateTime'; - } - - if ($this->resourceClassResolver->isResourceClass($className)) { - $resourceMetadata = $this->resourceMetadataFactory->create($className); - - return $resourceMetadata->getIri() ?? "#{$resourceMetadata->getShortName()}"; - } - break; - } - - return null; - } - - /** - * Builds the classes array. - */ - private function getClasses(array $entrypointProperties, array $classes): array - { - $classes[] = [ - '@id' => '#Entrypoint', - '@type' => 'hydra:Class', - 'hydra:title' => 'The API entrypoint', - 'hydra:supportedProperty' => $entrypointProperties, - 'hydra:supportedOperation' => [ - '@type' => 'hydra:Operation', - 'hydra:method' => 'GET', - 'rdfs:label' => 'The API entrypoint.', - 'returns' => '#EntryPoint', - ], - ]; - - // Constraint violation - $classes[] = [ - '@id' => '#ConstraintViolation', - '@type' => 'hydra:Class', - 'hydra:title' => 'A constraint violation', - 'hydra:supportedProperty' => [ - [ - '@type' => 'hydra:SupportedProperty', - 'hydra:property' => [ - '@id' => '#ConstraintViolation/propertyPath', - '@type' => 'rdf:Property', - 'rdfs:label' => 'propertyPath', - 'domain' => '#ConstraintViolation', - 'range' => 'xmls:string', - ], - 'hydra:title' => 'propertyPath', - 'hydra:description' => 'The property path of the violation', - 'hydra:readable' => true, - 'hydra:writable' => false, - ], - [ - '@type' => 'hydra:SupportedProperty', - 'hydra:property' => [ - '@id' => '#ConstraintViolation/message', - '@type' => 'rdf:Property', - 'rdfs:label' => 'message', - 'domain' => '#ConstraintViolation', - 'range' => 'xmls:string', - ], - 'hydra:title' => 'message', - 'hydra:description' => 'The message associated with the violation', - 'hydra:readable' => true, - 'hydra:writable' => false, - ], - ], - ]; - - // Constraint violation list - $classes[] = [ - '@id' => '#ConstraintViolationList', - '@type' => 'hydra:Class', - 'subClassOf' => 'hydra:Error', - 'hydra:title' => 'A constraint violation list', - 'hydra:supportedProperty' => [ - [ - '@type' => 'hydra:SupportedProperty', - 'hydra:property' => [ - '@id' => '#ConstraintViolationList/violations', - '@type' => 'rdf:Property', - 'rdfs:label' => 'violations', - 'domain' => '#ConstraintViolationList', - 'range' => '#ConstraintViolation', - ], - 'hydra:title' => 'violations', - 'hydra:description' => 'The violations', - 'hydra:readable' => true, - 'hydra:writable' => false, - ], - ], - ]; - - return $classes; - } - - /** - * Gets a property definition. - */ - private function getProperty(PropertyMetadata $propertyMetadata, string $propertyName, string $prefixedShortName, string $shortName): array - { - $propertyData = [ - '@id' => $propertyMetadata->getIri() ?? "#$shortName/$propertyName", - '@type' => false === $propertyMetadata->isReadableLink() ? 'hydra:Link' : 'rdf:Property', - 'rdfs:label' => $propertyName, - 'domain' => $prefixedShortName, - ]; - - $type = $propertyMetadata->getType(); - - if (null !== $type && !$type->isCollection() && (null !== $className = $type->getClassName()) && $this->resourceClassResolver->isResourceClass($className)) { - $propertyData['owl:maxCardinality'] = 1; - } - - $property = [ - '@type' => 'hydra:SupportedProperty', - 'hydra:property' => $propertyData, - 'hydra:title' => $propertyName, - 'hydra:required' => $propertyMetadata->isRequired(), - 'hydra:readable' => $propertyMetadata->isReadable(), - 'hydra:writable' => $propertyMetadata->isWritable() || $propertyMetadata->isInitializable(), - ]; - - if (null !== $range = $this->getRange($propertyMetadata)) { - $property['hydra:property']['range'] = $range; - } - - if (null !== $description = $propertyMetadata->getDescription()) { - $property['hydra:description'] = $description; - } - - if ($propertyMetadata->getAttribute('deprecation_reason')) { - $property['owl:deprecated'] = true; - } - - return $property; - } - - /** - * Computes the documentation. - */ - private function computeDoc(Documentation $object, array $classes): array - { - $doc = ['@context' => $this->getContext(), '@id' => $this->urlGenerator->generate('api_doc', ['_format' => self::FORMAT]), '@type' => 'hydra:ApiDocumentation']; - - if ('' !== $object->getTitle()) { - $doc['hydra:title'] = $object->getTitle(); - } - - if ('' !== $object->getDescription()) { - $doc['hydra:description'] = $object->getDescription(); - } - - $doc['hydra:entrypoint'] = $this->urlGenerator->generate('api_entrypoint'); - $doc['hydra:supportedClass'] = $classes; - - return $doc; - } - - /** - * Builds the JSON-LD context for the API documentation. - */ - private function getContext(): array - { - return [ - '@vocab' => $this->urlGenerator->generate('api_doc', ['_format' => self::FORMAT], UrlGeneratorInterface::ABS_URL).'#', - 'hydra' => ContextBuilderInterface::HYDRA_NS, - 'rdf' => ContextBuilderInterface::RDF_NS, - 'rdfs' => ContextBuilderInterface::RDFS_NS, - 'xmls' => ContextBuilderInterface::XML_NS, - 'owl' => ContextBuilderInterface::OWL_NS, - 'schema' => ContextBuilderInterface::SCHEMA_ORG_NS, - 'domain' => ['@id' => 'rdfs:domain', '@type' => '@id'], - 'range' => ['@id' => 'rdfs:range', '@type' => '@id'], - 'subClassOf' => ['@id' => 'rdfs:subClassOf', '@type' => '@id'], - 'expects' => ['@id' => 'hydra:expects', '@type' => '@id'], - 'returns' => ['@id' => 'hydra:returns', '@type' => '@id'], - ]; - } - - /** - * {@inheritdoc} - */ - public function supportsNormalization($data, $format = null, array $context = []) - { - return self::FORMAT === $format && $data instanceof Documentation; - } - - /** - * {@inheritdoc} - */ - public function hasCacheableSupportsMethod(): bool - { - return true; - } -} diff --git a/src/Hydra/EntrypointNormalizer.php b/src/Hydra/EntrypointNormalizer.php deleted file mode 100644 index 2c2485f..0000000 --- a/src/Hydra/EntrypointNormalizer.php +++ /dev/null @@ -1,86 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace Zfegg\ApiSerializerExt\Hydra\Serializer; - -use Zfegg\ApiSerializerExt\Api\Entrypoint; -use Zfegg\ApiSerializerExt\Api\IriConverterInterface; -use Zfegg\ApiSerializerExt\Api\UrlGeneratorInterface; -use Zfegg\ApiSerializerExt\Exception\InvalidArgumentException; -use Zfegg\ApiSerializerExt\Metadata\Resource\Factory\ResourceMetadataFactoryInterface; -use Symfony\Component\Serializer\Normalizer\CacheableSupportsMethodInterface; -use Symfony\Component\Serializer\Normalizer\NormalizerInterface; - -/** - * Normalizes the API entrypoint. - * - * @author Kévin Dunglas - */ -final class EntrypointNormalizer implements NormalizerInterface, CacheableSupportsMethodInterface -{ - public const FORMAT = 'jsonld'; - - private $resourceMetadataFactory; - private $iriConverter; - private $urlGenerator; - - public function __construct(ResourceMetadataFactoryInterface $resourceMetadataFactory, IriConverterInterface $iriConverter, UrlGeneratorInterface $urlGenerator) - { - $this->resourceMetadataFactory = $resourceMetadataFactory; - $this->iriConverter = $iriConverter; - $this->urlGenerator = $urlGenerator; - } - - /** - * {@inheritdoc} - */ - public function normalize($object, $format = null, array $context = []) - { - $entrypoint = [ - '@context' => $this->urlGenerator->generate('api_jsonld_context', ['shortName' => 'Entrypoint']), - '@id' => $this->urlGenerator->generate('api_entrypoint'), - '@type' => 'Entrypoint', - ]; - - foreach ($object->getResourceNameCollection() as $resourceClass) { - $resourceMetadata = $this->resourceMetadataFactory->create($resourceClass); - - if (empty($resourceMetadata->getCollectionOperations())) { - continue; - } - try { - $entrypoint[lcfirst($resourceMetadata->getShortName())] = $this->iriConverter->getIriFromResourceClass($resourceClass); - } catch (InvalidArgumentException $ex) { - // Ignore resources without GET operations - } - } - - return $entrypoint; - } - - /** - * {@inheritdoc} - */ - public function supportsNormalization($data, $format = null, array $context = []) - { - return self::FORMAT === $format && $data instanceof Entrypoint; - } - - /** - * {@inheritdoc} - */ - public function hasCacheableSupportsMethod(): bool - { - return true; - } -} diff --git a/src/Hydra/ErrorNormalizer.php b/src/Hydra/ErrorNormalizer.php deleted file mode 100644 index c9f4799..0000000 --- a/src/Hydra/ErrorNormalizer.php +++ /dev/null @@ -1,81 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace Zfegg\ApiSerializerExt\Hydra\Serializer; - -use Zfegg\ApiSerializerExt\Api\UrlGeneratorInterface; -use Zfegg\ApiSerializerExt\Problem\Serializer\ErrorNormalizerTrait; -use Symfony\Component\Debug\Exception\FlattenException as LegacyFlattenException; -use Symfony\Component\ErrorHandler\Exception\FlattenException; -use Symfony\Component\Serializer\Normalizer\CacheableSupportsMethodInterface; -use Symfony\Component\Serializer\Normalizer\NormalizerInterface; - -/** - * Converts {@see \Exception} or {@see FlattenException} or {@see LegacyFlattenException} to a Hydra error representation. - * - * @author Kévin Dunglas - * @author Samuel ROZE - */ -final class ErrorNormalizer implements NormalizerInterface, CacheableSupportsMethodInterface -{ - use ErrorNormalizerTrait; - - public const FORMAT = 'jsonld'; - public const TITLE = 'title'; - - private $urlGenerator; - private $debug; - private $defaultContext = [self::TITLE => 'An error occurred']; - - public function __construct(UrlGeneratorInterface $urlGenerator, bool $debug = false, array $defaultContext = []) - { - $this->urlGenerator = $urlGenerator; - $this->debug = $debug; - $this->defaultContext = array_merge($this->defaultContext, $defaultContext); - } - - /** - * {@inheritdoc} - */ - public function normalize($object, $format = null, array $context = []) - { - $data = [ - '@context' => $this->urlGenerator->generate('api_jsonld_context', ['shortName' => 'Error']), - '@type' => 'hydra:Error', - 'hydra:title' => $context[self::TITLE] ?? $this->defaultContext[self::TITLE], - 'hydra:description' => $this->getErrorMessage($object, $context, $this->debug), - ]; - - if ($this->debug && null !== $trace = $object->getTrace()) { - $data['trace'] = $trace; - } - - return $data; - } - - /** - * {@inheritdoc} - */ - public function supportsNormalization($data, $format = null) - { - return self::FORMAT === $format && ($data instanceof \Exception || $data instanceof FlattenException || $data instanceof LegacyFlattenException); - } - - /** - * {@inheritdoc} - */ - public function hasCacheableSupportsMethod(): bool - { - return true; - } -} diff --git a/src/Hydra/EventListener/AddLinkHeaderListener.php b/src/Hydra/EventListener/AddLinkHeaderListener.php deleted file mode 100644 index e19dd74..0000000 --- a/src/Hydra/EventListener/AddLinkHeaderListener.php +++ /dev/null @@ -1,60 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace Zfegg\ApiSerializerExt\Hydra\EventListener; - -use Fig\Link\GenericLinkProvider; -use Fig\Link\Link; -use Zfegg\ApiSerializerExt\Api\UrlGeneratorInterface; -use Zfegg\ApiSerializerExt\JsonLd\ContextBuilder; -use Zfegg\ApiSerializerExt\Util\CorsTrait; -use Symfony\Component\HttpKernel\Event\ResponseEvent; - -/** - * Adds the HTTP Link header pointing to the Hydra documentation. - * - * @author Kévin Dunglas - */ -final class AddLinkHeaderListener -{ - use CorsTrait; - - private $urlGenerator; - - public function __construct(UrlGeneratorInterface $urlGenerator) - { - $this->urlGenerator = $urlGenerator; - } - - /** - * Sends the Hydra header on each response. - */ - public function onKernelResponse(ResponseEvent $event): void - { - $request = $event->getRequest(); - // Prevent issues with NelmioCorsBundle - if ($this->isPreflightRequest($request)) { - return; - } - - $apiDocUrl = $this->urlGenerator->generate('api_doc', ['_format' => 'jsonld'], UrlGeneratorInterface::ABS_URL); - $link = new Link(ContextBuilder::HYDRA_NS.'apiDocumentation', $apiDocUrl); - - if (null === $linkProvider = $request->attributes->get('_links')) { - $request->attributes->set('_links', new GenericLinkProvider([$link])); - - return; - } - $request->attributes->set('_links', $linkProvider->withLink($link)); - } -} diff --git a/src/Hydra/JsonSchema/SchemaFactory.php b/src/Hydra/JsonSchema/SchemaFactory.php deleted file mode 100644 index 3b2ef4b..0000000 --- a/src/Hydra/JsonSchema/SchemaFactory.php +++ /dev/null @@ -1,133 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace Zfegg\ApiSerializerExt\Hydra\JsonSchema; - -use Zfegg\ApiSerializerExt\JsonSchema\Schema; -use Zfegg\ApiSerializerExt\JsonSchema\SchemaFactory as BaseSchemaFactory; -use Zfegg\ApiSerializerExt\JsonSchema\SchemaFactoryInterface; - -/** - * Generates the JSON Schema corresponding to a Hydra document. - * - * @experimental - * - * @author Kévin Dunglas - */ -final class SchemaFactory implements SchemaFactoryInterface -{ - private const BASE_PROP = [ - 'readOnly' => true, - 'type' => 'string', - ]; - private const BASE_PROPS = [ - '@context' => self::BASE_PROP, - '@id' => self::BASE_PROP, - '@type' => self::BASE_PROP, - ]; - - private $schemaFactory; - - public function __construct(BaseSchemaFactory $schemaFactory) - { - $this->schemaFactory = $schemaFactory; - $schemaFactory->addDistinctFormat('jsonld'); - } - - /** - * {@inheritdoc} - */ - public function buildSchema(string $resourceClass, string $format = 'jsonld', string $type = Schema::TYPE_OUTPUT, ?string $operationType = null, ?string $operationName = null, ?Schema $schema = null, ?array $serializerContext = null, bool $forceCollection = false): Schema - { - $schema = $this->schemaFactory->buildSchema($resourceClass, $format, $type, $operationType, $operationName, $schema, $serializerContext, $forceCollection); - if ('jsonld' !== $format) { - return $schema; - } - - $definitions = $schema->getDefinitions(); - if ($key = $schema->getRootDefinitionKey()) { - $definitions[$key]['properties'] = self::BASE_PROPS + ($definitions[$key]['properties'] ?? []); - - return $schema; - } - - if (($schema['type'] ?? '') === 'array') { - // hydra:collection - $items = $schema['items']; - unset($schema['items']); - - $schema['type'] = 'object'; - $schema['properties'] = [ - 'hydra:member' => [ - 'type' => 'array', - 'items' => $items, - ], - 'hydra:totalItems' => [ - 'type' => 'integer', - 'minimum' => 0, - ], - 'hydra:view' => [ - 'type' => 'object', - 'properties' => [ - '@id' => [ - 'type' => 'string', - 'format' => 'iri-reference', - ], - '@type' => [ - 'type' => 'string', - ], - 'hydra:first' => [ - 'type' => 'string', - 'format' => 'iri-reference', - ], - 'hydra:last' => [ - 'type' => 'string', - 'format' => 'iri-reference', - ], - 'hydra:next' => [ - 'type' => 'string', - 'format' => 'iri-reference', - ], - ], - ], - 'hydra:search' => [ - 'type' => 'object', - 'properties' => [ - '@type' => ['type' => 'string'], - 'hydra:template' => ['type' => 'string'], - 'hydra:variableRepresentation' => ['type' => 'string'], - 'hydra:mapping' => [ - 'type' => 'array', - 'items' => [ - 'type' => 'object', - 'properties' => [ - '@type' => ['type' => 'string'], - 'variable' => ['type' => 'string'], - 'property' => ['type' => 'string'], - 'required' => ['type' => 'boolean'], - ], - ], - ], - ], - ], - ]; - $schema['required'] = [ - 'hydra:member', - ]; - - return $schema; - } - - return $schema; - } -} diff --git a/src/Hydra/PartialCollectionViewNormalizer.php b/src/Hydra/PartialCollectionViewNormalizer.php deleted file mode 100644 index a66ea4c..0000000 --- a/src/Hydra/PartialCollectionViewNormalizer.php +++ /dev/null @@ -1,167 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace Zfegg\ApiSerializerExt\Hydra\Serializer; - -use Zfegg\ApiSerializerExt\DataProvider\PaginatorInterface; -use Zfegg\ApiSerializerExt\DataProvider\PartialPaginatorInterface; -use Zfegg\ApiSerializerExt\Metadata\Resource\Factory\ResourceMetadataFactoryInterface; -use Zfegg\ApiSerializerExt\Util\IriHelper; -use Symfony\Component\PropertyAccess\PropertyAccess; -use Symfony\Component\PropertyAccess\PropertyAccessorInterface; -use Symfony\Component\Serializer\Exception\UnexpectedValueException; -use Symfony\Component\Serializer\Normalizer\CacheableSupportsMethodInterface; -use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface; -use Symfony\Component\Serializer\Normalizer\NormalizerInterface; - -/** - * Adds a view key to the result of a paginated Hydra collection. - * - * @author Kévin Dunglas - * @author Samuel ROZE - */ -final class PartialCollectionViewNormalizer implements NormalizerInterface, NormalizerAwareInterface, CacheableSupportsMethodInterface -{ - private $collectionNormalizer; - private $pageParameterName; - private $enabledParameterName; - private $resourceMetadataFactory; - private $propertyAccessor; - - public function __construct(NormalizerInterface $collectionNormalizer, string $pageParameterName = 'page', string $enabledParameterName = 'pagination', ResourceMetadataFactoryInterface $resourceMetadataFactory = null, PropertyAccessorInterface $propertyAccessor = null) - { - $this->collectionNormalizer = $collectionNormalizer; - $this->pageParameterName = $pageParameterName; - $this->enabledParameterName = $enabledParameterName; - $this->resourceMetadataFactory = $resourceMetadataFactory; - $this->propertyAccessor = $propertyAccessor ?? PropertyAccess::createPropertyAccessor(); - } - - /** - * {@inheritdoc} - */ - public function normalize($object, $format = null, array $context = []) - { - $data = $this->collectionNormalizer->normalize($object, $format, $context); - if (!\is_array($data)) { - throw new UnexpectedValueException('Expected data to be an array'); - } - - if (isset($context['api_sub_level'])) { - return $data; - } - - $currentPage = $lastPage = $itemsPerPage = $pageTotalItems = null; - if ($paginated = $object instanceof PartialPaginatorInterface) { - if ($object instanceof PaginatorInterface) { - $paginated = 1. !== $lastPage = $object->getLastPage(); - } else { - $itemsPerPage = $object->getItemsPerPage(); - $pageTotalItems = (float) \count($object); - } - - $currentPage = $object->getCurrentPage(); - } - - $parsed = IriHelper::parseIri($context['request_uri'] ?? '/', $this->pageParameterName); - $appliedFilters = $parsed['parameters']; - unset($appliedFilters[$this->enabledParameterName]); - - if (!$appliedFilters && !$paginated) { - return $data; - } - - $metadata = isset($context['resource_class']) && null !== $this->resourceMetadataFactory ? $this->resourceMetadataFactory->create($context['resource_class']) : null; - $isPaginatedWithCursor = $paginated && null !== $metadata && null !== $cursorPaginationAttribute = $metadata->getCollectionOperationAttribute($context['collection_operation_name'] ?? $context['subresource_operation_name'], 'pagination_via_cursor', null, true); - - $data['hydra:view'] = [ - '@id' => IriHelper::createIri($parsed['parts'], $parsed['parameters'], $this->pageParameterName, $paginated && !$isPaginatedWithCursor ? $currentPage : null), - '@type' => 'hydra:PartialCollectionView', - ]; - - if ($isPaginatedWithCursor) { - $objects = iterator_to_array($object); - $firstObject = current($objects); - $lastObject = end($objects); - - $data['hydra:view']['@id'] = IriHelper::createIri($parsed['parts'], $parsed['parameters']); - - if (false !== $lastObject && isset($cursorPaginationAttribute)) { - $data['hydra:view']['hydra:next'] = IriHelper::createIri($parsed['parts'], array_merge($parsed['parameters'], $this->cursorPaginationFields($cursorPaginationAttribute, 1, $lastObject))); - } - - if (false !== $firstObject && isset($cursorPaginationAttribute)) { - $data['hydra:view']['hydra:previous'] = IriHelper::createIri($parsed['parts'], array_merge($parsed['parameters'], $this->cursorPaginationFields($cursorPaginationAttribute, -1, $firstObject))); - } - } elseif ($paginated) { - if (null !== $lastPage) { - $data['hydra:view']['hydra:first'] = IriHelper::createIri($parsed['parts'], $parsed['parameters'], $this->pageParameterName, 1.); - $data['hydra:view']['hydra:last'] = IriHelper::createIri($parsed['parts'], $parsed['parameters'], $this->pageParameterName, $lastPage); - } - - if (1. !== $currentPage) { - $data['hydra:view']['hydra:previous'] = IriHelper::createIri($parsed['parts'], $parsed['parameters'], $this->pageParameterName, $currentPage - 1.); - } - - if (null !== $lastPage && $currentPage !== $lastPage || null === $lastPage && $pageTotalItems >= $itemsPerPage) { - $data['hydra:view']['hydra:next'] = IriHelper::createIri($parsed['parts'], $parsed['parameters'], $this->pageParameterName, $currentPage + 1.); - } - } - - return $data; - } - - /** - * {@inheritdoc} - */ - public function supportsNormalization($data, $format = null) - { - return $this->collectionNormalizer->supportsNormalization($data, $format); - } - - /** - * {@inheritdoc} - */ - public function hasCacheableSupportsMethod(): bool - { - return $this->collectionNormalizer instanceof CacheableSupportsMethodInterface && $this->collectionNormalizer->hasCacheableSupportsMethod(); - } - - /** - * {@inheritdoc} - */ - public function setNormalizer(NormalizerInterface $normalizer) - { - if ($this->collectionNormalizer instanceof NormalizerAwareInterface) { - $this->collectionNormalizer->setNormalizer($normalizer); - } - } - - private function cursorPaginationFields(array $fields, int $direction, $object) - { - $paginationFilters = []; - - foreach ($fields as $field) { - $forwardRangeOperator = 'desc' === strtolower($field['direction']) ? 'lt' : 'gt'; - $backwardRangeOperator = 'gt' === $forwardRangeOperator ? 'lt' : 'gt'; - - $operator = $direction > 0 ? $forwardRangeOperator : $backwardRangeOperator; - - $paginationFilters[$field['field']] = [ - $operator => (string) $this->propertyAccessor->getValue($object, $field['field']), - ]; - } - - return $paginationFilters; - } -} diff --git a/src/JsonApi/EventListener/TransformFieldsetsParametersListener.php b/src/JsonApi/EventListener/TransformFieldsetsParametersListener.php deleted file mode 100644 index 65d8fe2..0000000 --- a/src/JsonApi/EventListener/TransformFieldsetsParametersListener.php +++ /dev/null @@ -1,82 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace Zfegg\ApiSerializerExt\JsonApi\EventListener; - -use Zfegg\ApiSerializerExt\Metadata\Resource\Factory\ResourceMetadataFactoryInterface; -use Symfony\Component\HttpKernel\Event\RequestEvent; - -/** - * @see http://jsonapi.org/format/#fetching-sparse-fieldsets - * @see https://api-platform.com/docs/core/filters#property-filter - * - * @author Baptiste Meyer - */ -final class TransformFieldsetsParametersListener -{ - private $resourceMetadataFactory; - - public function __construct(ResourceMetadataFactoryInterface $resourceMetadataFactory) - { - $this->resourceMetadataFactory = $resourceMetadataFactory; - } - - public function onKernelRequest(RequestEvent $event): void - { - $request = $event->getRequest(); - - $includeParameter = $request->query->get('include'); - if ( - 'jsonapi' !== $request->getRequestFormat() || - !($resourceClass = $request->attributes->get('_api_resource_class')) || - (!($fieldsParameter = $request->query->get('fields')) && !$includeParameter) - ) { - return; - } - - if ( - ($fieldsParameter && !\is_array($fieldsParameter)) || - ($includeParameter && !\is_string($includeParameter)) - ) { - return; - } - - $properties = []; - - $includeParameter = explode(',', $includeParameter ?? ''); - - if (!$fieldsParameter) { - $request->attributes->set('_api_included', $includeParameter); - - return; - } - - $resourceShortName = $this->resourceMetadataFactory->create($resourceClass)->getShortName(); - - foreach ($fieldsParameter as $resourceType => $fields) { - $fields = explode(',', $fields); - - if ($resourceShortName === $resourceType) { - $properties = array_merge($properties, $fields); - } elseif (\in_array($resourceType, $includeParameter, true)) { - $properties[$resourceType] = $fields; - - $request->attributes->set('_api_included', array_merge($request->attributes->get('_api_included', []), [$resourceType])); - } else { - $properties[$resourceType] = $fields; - } - } - - $request->attributes->set('_api_filter_property', $properties); - } -} diff --git a/src/JsonApi/EventListener/TransformFilteringParametersListener.php b/src/JsonApi/EventListener/TransformFilteringParametersListener.php deleted file mode 100644 index 955749d..0000000 --- a/src/JsonApi/EventListener/TransformFilteringParametersListener.php +++ /dev/null @@ -1,40 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace Zfegg\ApiSerializerExt\JsonApi\EventListener; - -use Symfony\Component\HttpKernel\Event\RequestEvent; - -/** - * @see http://jsonapi.org/format/#fetching-filtering - * @see http://jsonapi.org/recommendations/#filtering - * - * @author Héctor Hurtarte - * @author Baptiste Meyer - */ -final class TransformFilteringParametersListener -{ - public function onKernelRequest(RequestEvent $event): void - { - $request = $event->getRequest(); - if ( - 'jsonapi' !== $request->getRequestFormat() || - null === ($filterParameter = $request->query->get('filter')) || - !\is_array($filterParameter) - ) { - return; - } - $filters = $request->attributes->get('_api_filters', []); - $request->attributes->set('_api_filters', array_merge($filterParameter, $filters)); - } -} diff --git a/src/JsonApi/EventListener/TransformPaginationParametersListener.php b/src/JsonApi/EventListener/TransformPaginationParametersListener.php deleted file mode 100644 index 5f1015a..0000000 --- a/src/JsonApi/EventListener/TransformPaginationParametersListener.php +++ /dev/null @@ -1,45 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace Zfegg\ApiSerializerExt\JsonApi\EventListener; - -use Symfony\Component\HttpKernel\Event\RequestEvent; - -/** - * @see http://jsonapi.org/format/#fetching-pagination - * @see https://api-platform.com/docs/core/pagination - * - * @author Héctor Hurtarte - * @author Baptiste Meyer - */ -final class TransformPaginationParametersListener -{ - public function onKernelRequest(RequestEvent $event): void - { - $request = $event->getRequest(); - - if ( - 'jsonapi' !== $request->getRequestFormat() || - null === ($pageParameter = $request->query->get('page')) || - !\is_array($pageParameter) - ) { - return; - } - - $filters = $request->attributes->get('_api_filters', []); - $request->attributes->set('_api_filters', array_merge($pageParameter, $filters)); - - /* @TODO remove the `_api_pagination` attribute in 3.0 (@meyerbaptiste) */ - $request->attributes->set('_api_pagination', $pageParameter); - } -} diff --git a/src/JsonApi/EventListener/TransformSortingParametersListener.php b/src/JsonApi/EventListener/TransformSortingParametersListener.php deleted file mode 100644 index 74ae77c..0000000 --- a/src/JsonApi/EventListener/TransformSortingParametersListener.php +++ /dev/null @@ -1,64 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace Zfegg\ApiSerializerExt\JsonApi\EventListener; - -use Symfony\Component\HttpKernel\Event\RequestEvent; - -/** - * @see http://jsonapi.org/format/#fetching-sorting - * @see https://api-platform.com/docs/core/filters#order-filter - * - * @author Héctor Hurtarte - * @author Baptiste Meyer - */ -final class TransformSortingParametersListener -{ - private $orderParameterName; - - public function __construct(string $orderParameterName = 'order') - { - $this->orderParameterName = $orderParameterName; - } - - public function onKernelRequest(RequestEvent $event): void - { - $request = $event->getRequest(); - - if ( - 'jsonapi' !== $request->getRequestFormat() || - null === ($orderParameter = $request->query->get('sort')) || - \is_array($orderParameter) - ) { - return; - } - - $orderParametersArray = explode(',', $orderParameter); - $transformedOrderParametersArray = []; - - foreach ($orderParametersArray as $orderParameter) { - $sorting = 'asc'; - - if ('-' === ($orderParameter[0] ?? null)) { - $sorting = 'desc'; - $orderParameter = substr($orderParameter, 1); - } - - $transformedOrderParametersArray[$orderParameter] = $sorting; - } - - $filters = $request->attributes->get('_api_filters', []); - $filters[$this->orderParameterName] = $transformedOrderParametersArray; - $request->attributes->set('_api_filters', $filters); - } -} diff --git a/src/JsonApi/Serializer/CollectionNormalizer.php b/src/JsonApi/Serializer/CollectionNormalizer.php deleted file mode 100644 index 0c53969..0000000 --- a/src/JsonApi/Serializer/CollectionNormalizer.php +++ /dev/null @@ -1,102 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace Zfegg\ApiSerializerExt\JsonApi\Serializer; - -use Zfegg\ApiSerializerExt\Serializer\AbstractCollectionNormalizer; -use Zfegg\ApiSerializerExt\Util\IriHelper; -use Symfony\Component\Serializer\Exception\UnexpectedValueException; - -/** - * Normalizes collections in the JSON API format. - * - * @author Kevin Dunglas - * @author Hamza Amrouche - * @author Baptiste Meyer - */ -final class CollectionNormalizer extends AbstractCollectionNormalizer -{ - public const FORMAT = 'jsonapi'; - - /** - * {@inheritdoc} - */ - protected function getPaginationData($object, array $context = []): array - { - [$paginator, $paginated, $currentPage, $itemsPerPage, $lastPage, $pageTotalItems, $totalItems] = $this->getPaginationConfig($object, $context); - $parsed = IriHelper::parseIri($context['request_uri'] ?? '/', $this->pageParameterName); - - $data = [ - 'links' => [ - 'self' => IriHelper::createIri($parsed['parts'], $parsed['parameters'], $this->pageParameterName, $paginated ? $currentPage : null), - ], - ]; - - if ($paginated) { - if (null !== $lastPage) { - $data['links']['first'] = IriHelper::createIri($parsed['parts'], $parsed['parameters'], $this->pageParameterName, 1.); - $data['links']['last'] = IriHelper::createIri($parsed['parts'], $parsed['parameters'], $this->pageParameterName, $lastPage); - } - - if (1. !== $currentPage) { - $data['links']['prev'] = IriHelper::createIri($parsed['parts'], $parsed['parameters'], $this->pageParameterName, $currentPage - 1.); - } - - if (null !== $lastPage && $currentPage !== $lastPage || null === $lastPage && $pageTotalItems >= $itemsPerPage) { - $data['links']['next'] = IriHelper::createIri($parsed['parts'], $parsed['parameters'], $this->pageParameterName, $currentPage + 1.); - } - } - - if (null !== $totalItems) { - $data['meta']['totalItems'] = $totalItems; - } - - if ($paginator) { - $data['meta']['itemsPerPage'] = (int) $itemsPerPage; - $data['meta']['currentPage'] = (int) $currentPage; - } - - return $data; - } - - /** - * {@inheritdoc} - * - * @throws UnexpectedValueException - */ - protected function getItemsData($object, string $format = null, array $context = []): array - { - $data = [ - 'data' => [], - ]; - - foreach ($object as $obj) { - $item = $this->normalizer->normalize($obj, $format, $context); - if (!\is_array($item)) { - throw new UnexpectedValueException('Expected item to be an array'); - } - - if (!isset($item['data'])) { - throw new UnexpectedValueException('The JSON API document must contain a "data" key.'); - } - - $data['data'][] = $item['data']; - - if (isset($item['included'])) { - $data['included'] = array_values(array_unique(array_merge($data['included'] ?? [], $item['included']), SORT_REGULAR)); - } - } - - return $data; - } -} diff --git a/src/JsonApi/Serializer/ConstraintViolationListNormalizer.php b/src/JsonApi/Serializer/ConstraintViolationListNormalizer.php deleted file mode 100644 index 74fab2d..0000000 --- a/src/JsonApi/Serializer/ConstraintViolationListNormalizer.php +++ /dev/null @@ -1,98 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace Zfegg\ApiSerializerExt\JsonApi\Serializer; - -use Zfegg\ApiSerializerExt\Metadata\Property\Factory\PropertyMetadataFactoryInterface; -use Symfony\Component\Serializer\NameConverter\NameConverterInterface; -use Symfony\Component\Serializer\Normalizer\CacheableSupportsMethodInterface; -use Symfony\Component\Serializer\Normalizer\NormalizerInterface; -use Symfony\Component\Validator\ConstraintViolationInterface; -use Symfony\Component\Validator\ConstraintViolationListInterface; - -/** - * Converts {@see \Symfony\Component\Validator\ConstraintViolationListInterface} to a JSON API error representation. - * - * @author Héctor Hurtarte - */ -final class ConstraintViolationListNormalizer implements NormalizerInterface, CacheableSupportsMethodInterface -{ - public const FORMAT = 'jsonapi'; - - private $nameConverter; - private $propertyMetadataFactory; - - public function __construct(PropertyMetadataFactoryInterface $propertyMetadataFactory, NameConverterInterface $nameConverter = null) - { - $this->propertyMetadataFactory = $propertyMetadataFactory; - $this->nameConverter = $nameConverter; - } - - public function normalize($object, $format = null, array $context = []) - { - $violations = []; - foreach ($object as $violation) { - $violations[] = [ - 'detail' => $violation->getMessage(), - 'source' => [ - 'pointer' => $this->getSourcePointerFromViolation($violation), - ], - ]; - } - - return ['errors' => $violations]; - } - - /** - * {@inheritdoc} - */ - public function supportsNormalization($data, $format = null) - { - return self::FORMAT === $format && $data instanceof ConstraintViolationListInterface; - } - - /** - * {@inheritdoc} - */ - public function hasCacheableSupportsMethod(): bool - { - return true; - } - - private function getSourcePointerFromViolation(ConstraintViolationInterface $violation) - { - $fieldName = $violation->getPropertyPath(); - - if (!$fieldName) { - return 'data'; - } - - $class = \get_class($violation->getRoot()); - $propertyMetadata = $this->propertyMetadataFactory - ->create( - // Im quite sure this requires some thought in case of validations over relationships - $class, - $fieldName - ); - - if (null !== $this->nameConverter) { - $fieldName = $this->nameConverter->normalize($fieldName, $class, self::FORMAT); - } - - if (($type = $propertyMetadata->getType()) && null !== $type->getClassName()) { - return "data/relationships/$fieldName"; - } - - return "data/attributes/$fieldName"; - } -} diff --git a/src/JsonApi/Serializer/EntrypointNormalizer.php b/src/JsonApi/Serializer/EntrypointNormalizer.php deleted file mode 100644 index 179c098..0000000 --- a/src/JsonApi/Serializer/EntrypointNormalizer.php +++ /dev/null @@ -1,84 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace Zfegg\ApiSerializerExt\JsonApi\Serializer; - -use Zfegg\ApiSerializerExt\Api\Entrypoint; -use Zfegg\ApiSerializerExt\Api\IriConverterInterface; -use Zfegg\ApiSerializerExt\Api\UrlGeneratorInterface; -use Zfegg\ApiSerializerExt\Exception\InvalidArgumentException; -use Zfegg\ApiSerializerExt\Metadata\Resource\Factory\ResourceMetadataFactoryInterface; -use Symfony\Component\Serializer\Normalizer\CacheableSupportsMethodInterface; -use Symfony\Component\Serializer\Normalizer\NormalizerInterface; - -/** - * Normalizes the API entrypoint. - * - * @author Amrouche Hamza - * @author Kévin Dunglas - */ -final class EntrypointNormalizer implements NormalizerInterface, CacheableSupportsMethodInterface -{ - public const FORMAT = 'jsonapi'; - - private $resourceMetadataFactory; - private $iriConverter; - private $urlGenerator; - - public function __construct(ResourceMetadataFactoryInterface $resourceMetadataFactory, IriConverterInterface $iriConverter, UrlGeneratorInterface $urlGenerator) - { - $this->resourceMetadataFactory = $resourceMetadataFactory; - $this->iriConverter = $iriConverter; - $this->urlGenerator = $urlGenerator; - } - - /** - * {@inheritdoc} - */ - public function normalize($object, $format = null, array $context = []) - { - $entrypoint = ['links' => ['self' => $this->urlGenerator->generate('api_entrypoint', [], UrlGeneratorInterface::ABS_URL)]]; - - foreach ($object->getResourceNameCollection() as $resourceClass) { - $resourceMetadata = $this->resourceMetadataFactory->create($resourceClass); - - if (!$resourceMetadata->getCollectionOperations()) { - continue; - } - - try { - $entrypoint['links'][lcfirst($resourceMetadata->getShortName())] = $this->iriConverter->getIriFromResourceClass($resourceClass, UrlGeneratorInterface::ABS_URL); - } catch (InvalidArgumentException $ex) { - // Ignore resources without GET operations - } - } - - return $entrypoint; - } - - /** - * {@inheritdoc} - */ - public function supportsNormalization($data, $format = null) - { - return self::FORMAT === $format && $data instanceof Entrypoint; - } - - /** - * {@inheritdoc} - */ - public function hasCacheableSupportsMethod(): bool - { - return true; - } -} diff --git a/src/JsonApi/Serializer/ErrorNormalizer.php b/src/JsonApi/Serializer/ErrorNormalizer.php deleted file mode 100644 index 1ad0ba4..0000000 --- a/src/JsonApi/Serializer/ErrorNormalizer.php +++ /dev/null @@ -1,74 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace Zfegg\ApiSerializerExt\JsonApi\Serializer; - -use Zfegg\ApiSerializerExt\Problem\Serializer\ErrorNormalizerTrait; -use Symfony\Component\Debug\Exception\FlattenException as LegacyFlattenException; -use Symfony\Component\ErrorHandler\Exception\FlattenException; -use Symfony\Component\Serializer\Normalizer\CacheableSupportsMethodInterface; -use Symfony\Component\Serializer\Normalizer\NormalizerInterface; - -/** - * Converts {@see \Exception} or {@see FlattenException} or {@see LegacyFlattenException} to a JSON API error representation. - * - * @author Héctor Hurtarte - */ -final class ErrorNormalizer implements NormalizerInterface, CacheableSupportsMethodInterface -{ - use ErrorNormalizerTrait; - - public const FORMAT = 'jsonapi'; - public const TITLE = 'title'; - - private $debug; - private $defaultContext = [ - self::TITLE => 'An error occurred', - ]; - - public function __construct(bool $debug = false, array $defaultContext = []) - { - $this->debug = $debug; - $this->defaultContext = array_merge($this->defaultContext, $defaultContext); - } - - public function normalize($object, $format = null, array $context = []) - { - $data = [ - 'title' => $context[self::TITLE] ?? $this->defaultContext[self::TITLE], - 'description' => $this->getErrorMessage($object, $context, $this->debug), - ]; - - if ($this->debug && null !== $trace = $object->getTrace()) { - $data['trace'] = $trace; - } - - return $data; - } - - /** - * {@inheritdoc} - */ - public function supportsNormalization($data, $format = null) - { - return self::FORMAT === $format && ($data instanceof \Exception || $data instanceof FlattenException || $data instanceof LegacyFlattenException); - } - - /** - * {@inheritdoc} - */ - public function hasCacheableSupportsMethod(): bool - { - return true; - } -} diff --git a/src/JsonApi/Serializer/ItemNormalizer.php b/src/JsonApi/Serializer/ItemNormalizer.php deleted file mode 100644 index 667fd6d..0000000 --- a/src/JsonApi/Serializer/ItemNormalizer.php +++ /dev/null @@ -1,109 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace Zfegg\ApiSerializerExt\JsonApi\Serializer; - -use Zfegg\ApiSerializerExt\Api\IriConverterInterface; -use Zfegg\ApiSerializerExt\Api\ResourceClassResolverInterface; -use Zfegg\ApiSerializerExt\Metadata\Property\Factory\PropertyMetadataFactoryInterface; -use Zfegg\ApiSerializerExt\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; -use Zfegg\ApiSerializerExt\Metadata\Property\PropertyMetadata; -use Zfegg\ApiSerializerExt\Metadata\Resource\Factory\ResourceMetadataFactoryInterface; -use Zfegg\ApiSerializerExt\Serializer\AbstractItemNormalizer; -use Zfegg\ApiSerializerExt\Serializer\CacheKeyTrait; -use Zfegg\ApiSerializerExt\Serializer\ContextTrait; -use Zfegg\ApiSerializerExt\Util\ClassInfoTrait; -use Symfony\Component\PropertyAccess\PropertyAccessorInterface; -use Symfony\Component\Serializer\Exception\NotNormalizableValueException; - -/** - * Converts between objects and array. - * - * @author Kévin Dunglas - * @author Amrouche Hamza - * @author Baptiste Meyer - */ -final class ItemNormalizer extends AbstractItemNormalizer -{ - use ClassInfoTrait; - - public const FORMAT = 'jsonapi'; - - private $componentsCache = []; - - /** - * {@inheritdoc} - */ - public function supportsNormalization($data, $format = null): bool - { - return self::FORMAT === $format && parent::supportsNormalization($data, $format); - } - - /** - * {@inheritdoc} - */ - public function supportsDenormalization($data, $type, $format = null): bool - { - return self::FORMAT === $format && parent::supportsDenormalization($data, $type, $format); - } - - /** - * {@inheritdoc} - * - * @throws NotNormalizableValueException - */ - public function denormalize($data, $class, $format = null, array $context = []) - { - // Avoid issues with proxies if we populated the object - if (!isset($context[self::OBJECT_TO_POPULATE]) && isset($data['data']['id'])) { - if (true !== ($context['api_allow_update'] ?? true)) { - throw new NotNormalizableValueException('Update is not allowed for this operation.'); - } - - $context[self::OBJECT_TO_POPULATE] = $this->iriConverter->getItemFromIri( - $data['data']['id'], - $context + ['fetch_data' => false] - ); - } - - // Merge attributes and relationships, into format expected by the parent normalizer - $dataToDenormalize = array_merge( - $data['data']['attributes'] ?? [], - $data['data']['relationships'] ?? [] - ); - - return parent::denormalize( - $dataToDenormalize, - $class, - $format, - $context - ); - } - - /** - * {@inheritdoc} - */ - protected function setAttributeValue($object, $attribute, $value, $format = null, array $context = []): void - { - parent::setAttributeValue($object, $attribute, \is_array($value) && \array_key_exists('data', $value) ? $value['data'] : $value, $format, $context); - } - - /** - * {@inheritdoc} - */ - protected function isAllowedAttribute($classOrObject, $attribute, $format = null, array $context = []): bool - { - return preg_match('/^\\w[-\\w_]*$/', $attribute) && parent::isAllowedAttribute($classOrObject, $attribute, $format, $context); - } - -} diff --git a/src/JsonApi/Serializer/ObjectNormalizer.php b/src/JsonApi/Serializer/ObjectNormalizer.php deleted file mode 100644 index be08f83..0000000 --- a/src/JsonApi/Serializer/ObjectNormalizer.php +++ /dev/null @@ -1,98 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace Zfegg\ApiSerializerExt\JsonApi\Serializer; - -use Zfegg\ApiSerializerExt\Api\IriConverterInterface; -use Zfegg\ApiSerializerExt\Api\ResourceClassResolverInterface; -use Zfegg\ApiSerializerExt\Metadata\Resource\Factory\ResourceMetadataFactoryInterface; -use Zfegg\ApiSerializerExt\Util\ClassInfoTrait; -use Symfony\Component\Serializer\Normalizer\CacheableSupportsMethodInterface; -use Symfony\Component\Serializer\Normalizer\NormalizerInterface; - -/** - * Decorates the output with JSON API metadata when appropriate, but otherwise - * just passes through to the decorated normalizer. - */ -final class ObjectNormalizer implements NormalizerInterface, CacheableSupportsMethodInterface -{ - use ClassInfoTrait; - - public const FORMAT = 'jsonapi'; - - private $decorated; - private $iriConverter; - private $resourceClassResolver; - private $resourceMetadataFactory; - - public function __construct(NormalizerInterface $decorated, IriConverterInterface $iriConverter, ResourceClassResolverInterface $resourceClassResolver, ResourceMetadataFactoryInterface $resourceMetadataFactory) - { - $this->decorated = $decorated; - $this->iriConverter = $iriConverter; - $this->resourceClassResolver = $resourceClassResolver; - $this->resourceMetadataFactory = $resourceMetadataFactory; - } - - /** - * {@inheritdoc} - */ - public function supportsNormalization($data, $format = null, array $context = []): bool - { - return self::FORMAT === $format && $this->decorated->supportsNormalization($data, $format, $context); - } - - /** - * {@inheritdoc} - */ - public function hasCacheableSupportsMethod(): bool - { - return $this->decorated instanceof CacheableSupportsMethodInterface && $this->decorated->hasCacheableSupportsMethod(); - } - - /** - * {@inheritdoc} - */ - public function normalize($object, $format = null, array $context = []) - { - if (isset($context['api_resource'])) { - $originalResource = $context['api_resource']; - unset($context['api_resource']); - } - - $data = $this->decorated->normalize($object, $format, $context); - if (!\is_array($data) || isset($context['api_attribute'])) { - return $data; - } - - if (isset($originalResource)) { - $resourceClass = $this->resourceClassResolver->getResourceClass($originalResource); - $resourceMetadata = $this->resourceMetadataFactory->create($resourceClass); - - $resourceData = [ - 'id' => $this->iriConverter->getIriFromItem($originalResource), - 'type' => $resourceMetadata->getShortName(), - ]; - } else { - $resourceData = [ - 'id' => \function_exists('spl_object_id') ? spl_object_id($object) : spl_object_hash($object), - 'type' => (new \ReflectionClass($this->getObjectClass($object)))->getShortName(), - ]; - } - - if ($data) { - $resourceData['attributes'] = $data; - } - - return ['data' => $resourceData]; - } -} diff --git a/src/JsonApi/Serializer/ReservedAttributeNameConverter.php b/src/JsonApi/Serializer/ReservedAttributeNameConverter.php deleted file mode 100644 index eba052a..0000000 --- a/src/JsonApi/Serializer/ReservedAttributeNameConverter.php +++ /dev/null @@ -1,72 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace Zfegg\ApiSerializerExt\JsonApi\Serializer; - -use Symfony\Component\Serializer\NameConverter\AdvancedNameConverterInterface; -use Symfony\Component\Serializer\NameConverter\NameConverterInterface; - -/** - * Reserved attribute name converter. - * - * @author Baptiste Meyer - */ -final class ReservedAttributeNameConverter implements AdvancedNameConverterInterface -{ - public const JSON_API_RESERVED_ATTRIBUTES = [ - 'id' => '_id', - 'type' => '_type', - 'links' => '_links', - 'relationships' => '_relationships', - 'included' => '_included', - ]; - - private $nameConverter; - - public function __construct(NameConverterInterface $nameConverter = null) - { - $this->nameConverter = $nameConverter; - } - - /** - * {@inheritdoc} - */ - public function normalize($propertyName, string $class = null, string $format = null, array $context = []) - { - if (null !== $this->nameConverter) { - $propertyName = $this->nameConverter->normalize($propertyName, $class, $format, $context); - } - - if (isset(self::JSON_API_RESERVED_ATTRIBUTES[$propertyName])) { - $propertyName = self::JSON_API_RESERVED_ATTRIBUTES[$propertyName]; - } - - return $propertyName; - } - - /** - * {@inheritdoc} - */ - public function denormalize($propertyName, string $class = null, string $format = null, array $context = []) - { - if (\in_array($propertyName, self::JSON_API_RESERVED_ATTRIBUTES, true)) { - $propertyName = substr($propertyName, 1); - } - - if (null !== $this->nameConverter) { - $propertyName = $this->nameConverter->denormalize($propertyName, $class, $format, $context); - } - - return $propertyName; - } -} diff --git a/src/JsonLd/Action/ContextAction.php b/src/JsonLd/Action/ContextAction.php deleted file mode 100644 index d0eb95d..0000000 --- a/src/JsonLd/Action/ContextAction.php +++ /dev/null @@ -1,69 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace Zfegg\ApiSerializerExt\JsonLd\Action; - -use Zfegg\ApiSerializerExt\JsonLd\ContextBuilderInterface; -use Zfegg\ApiSerializerExt\Metadata\Resource\Factory\ResourceMetadataFactoryInterface; -use Zfegg\ApiSerializerExt\Metadata\Resource\Factory\ResourceNameCollectionFactoryInterface; -use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; - -/** - * Generates JSON-LD contexts. - * - * @author Kévin Dunglas - */ -final class ContextAction -{ - public const RESERVED_SHORT_NAMES = [ - 'ConstraintViolationList' => true, - 'Error' => true, - ]; - - private $contextBuilder; - private $resourceNameCollectionFactory; - private $resourceMetadataFactory; - - public function __construct(ContextBuilderInterface $contextBuilder, ResourceNameCollectionFactoryInterface $resourceNameCollectionFactory, ResourceMetadataFactoryInterface $resourceMetadataFactory) - { - $this->contextBuilder = $contextBuilder; - $this->resourceNameCollectionFactory = $resourceNameCollectionFactory; - $this->resourceMetadataFactory = $resourceMetadataFactory; - } - - /** - * Generates a context according to the type requested. - * - * @throws NotFoundHttpException - */ - public function __invoke(string $shortName): array - { - if ('Entrypoint' === $shortName) { - return ['@context' => $this->contextBuilder->getEntrypointContext()]; - } - - if (isset(self::RESERVED_SHORT_NAMES[$shortName])) { - return ['@context' => $this->contextBuilder->getBaseContext()]; - } - - foreach ($this->resourceNameCollectionFactory->create() as $resourceClass) { - $resourceMetadata = $this->resourceMetadataFactory->create($resourceClass); - - if ($shortName === $resourceMetadata->getShortName()) { - return ['@context' => $this->contextBuilder->getResourceContext($resourceClass)]; - } - } - - throw new NotFoundHttpException(); - } -} diff --git a/src/JsonLd/AnonymousContextBuilderInterface.php b/src/JsonLd/AnonymousContextBuilderInterface.php deleted file mode 100644 index e82577f..0000000 --- a/src/JsonLd/AnonymousContextBuilderInterface.php +++ /dev/null @@ -1,30 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace Zfegg\ApiSerializerExt\JsonLd; - -use Zfegg\ApiSerializerExt\Api\UrlGeneratorInterface; - -/** - * JSON-LD context builder with Input Output DTO support interface. - * - * @author Antoine Bluchet - */ -interface AnonymousContextBuilderInterface extends ContextBuilderInterface -{ - /** - * Creates a JSON-LD context based on the given object. - * Usually this is used with an Input or Output DTO object. - */ - public function getAnonymousResourceContext($object, array $context = [], int $referenceType = UrlGeneratorInterface::ABS_PATH): array; -} diff --git a/src/JsonLd/ContextBuilder.php b/src/JsonLd/ContextBuilder.php deleted file mode 100644 index 49c4920..0000000 --- a/src/JsonLd/ContextBuilder.php +++ /dev/null @@ -1,173 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace Zfegg\ApiSerializerExt\JsonLd; - -use Zfegg\ApiSerializerExt\Api\UrlGeneratorInterface; -use Zfegg\ApiSerializerExt\Metadata\Property\Factory\PropertyMetadataFactoryInterface; -use Zfegg\ApiSerializerExt\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; -use Zfegg\ApiSerializerExt\Metadata\Resource\Factory\ResourceMetadataFactoryInterface; -use Zfegg\ApiSerializerExt\Metadata\Resource\Factory\ResourceNameCollectionFactoryInterface; -use Zfegg\ApiSerializerExt\Util\ClassInfoTrait; -use Symfony\Component\Serializer\NameConverter\NameConverterInterface; - -/** - * {@inheritdoc} - * - * @author Kévin Dunglas - */ -final class ContextBuilder implements AnonymousContextBuilderInterface -{ - use ClassInfoTrait; - - public const FORMAT = 'jsonld'; - - private $resourceNameCollectionFactory; - private $resourceMetadataFactory; - private $propertyNameCollectionFactory; - private $propertyMetadataFactory; - private $urlGenerator; - - /** - * @var NameConverterInterface|null - */ - private $nameConverter; - - public function __construct(ResourceNameCollectionFactoryInterface $resourceNameCollectionFactory, ResourceMetadataFactoryInterface $resourceMetadataFactory, PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, UrlGeneratorInterface $urlGenerator, NameConverterInterface $nameConverter = null) - { - $this->resourceNameCollectionFactory = $resourceNameCollectionFactory; - $this->resourceMetadataFactory = $resourceMetadataFactory; - $this->propertyNameCollectionFactory = $propertyNameCollectionFactory; - $this->propertyMetadataFactory = $propertyMetadataFactory; - $this->urlGenerator = $urlGenerator; - $this->nameConverter = $nameConverter; - } - - /** - * {@inheritdoc} - */ - public function getBaseContext(int $referenceType = UrlGeneratorInterface::ABS_URL): array - { - return [ - '@vocab' => $this->urlGenerator->generate('api_doc', ['_format' => self::FORMAT], UrlGeneratorInterface::ABS_URL).'#', - 'hydra' => self::HYDRA_NS, - ]; - } - - /** - * {@inheritdoc} - */ - public function getEntrypointContext(int $referenceType = UrlGeneratorInterface::ABS_PATH): array - { - $context = $this->getBaseContext($referenceType); - - foreach ($this->resourceNameCollectionFactory->create() as $resourceClass) { - $resourceMetadata = $this->resourceMetadataFactory->create($resourceClass); - - $resourceName = lcfirst($resourceMetadata->getShortName()); - - $context[$resourceName] = [ - '@id' => 'Entrypoint/'.$resourceName, - '@type' => '@id', - ]; - } - - return $context; - } - - /** - * {@inheritdoc} - */ - public function getResourceContext(string $resourceClass, int $referenceType = UrlGeneratorInterface::ABS_PATH): array - { - $metadata = $this->resourceMetadataFactory->create($resourceClass); - if (null === $shortName = $metadata->getShortName()) { - return []; - } - - return $this->getResourceContextWithShortname($resourceClass, $referenceType, $shortName); - } - - /** - * {@inheritdoc} - */ - public function getResourceContextUri(string $resourceClass, int $referenceType = UrlGeneratorInterface::ABS_PATH): string - { - $resourceMetadata = $this->resourceMetadataFactory->create($resourceClass); - - return $this->urlGenerator->generate('api_jsonld_context', ['shortName' => $resourceMetadata->getShortName()], $referenceType); - } - - /** - * {@inheritdoc} - */ - public function getAnonymousResourceContext($object, array $context = [], int $referenceType = UrlGeneratorInterface::ABS_PATH): array - { - $outputClass = $this->getObjectClass($object); - $shortName = (new \ReflectionClass($outputClass))->getShortName(); - - $jsonLdContext = [ - '@context' => $this->getResourceContextWithShortname( - $outputClass, - $referenceType, - $shortName - ), - '@type' => $shortName, - '@id' => $context['iri'] ?? '_:'.(\function_exists('spl_object_id') ? spl_object_id($object) : spl_object_hash($object)), - ]; - - // here the object can be different from the resource given by the $context['api_resource'] value - if (isset($context['api_resource'])) { - $jsonLdContext['@type'] = $this->resourceMetadataFactory->create($this->getObjectClass($context['api_resource']))->getShortName(); - } - - return $jsonLdContext; - } - - private function getResourceContextWithShortname(string $resourceClass, int $referenceType, string $shortName): array - { - $context = $this->getBaseContext($referenceType); - - foreach ($this->propertyNameCollectionFactory->create($resourceClass) as $propertyName) { - $propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $propertyName); - - if ($propertyMetadata->isIdentifier() && true !== $propertyMetadata->isWritable()) { - continue; - } - - $convertedName = $this->nameConverter ? $this->nameConverter->normalize($propertyName, $resourceClass, self::FORMAT) : $propertyName; - $jsonldContext = $propertyMetadata->getAttributes()['jsonld_context'] ?? []; - - if (!$id = $propertyMetadata->getIri()) { - $id = sprintf('%s/%s', $shortName, $convertedName); - } - - if (false === $propertyMetadata->isReadableLink()) { - $jsonldContext += [ - '@id' => $id, - '@type' => '@id', - ]; - } - - if (empty($jsonldContext)) { - $context[$convertedName] = $id; - } else { - $context[$convertedName] = $jsonldContext + [ - '@id' => $id, - ]; - } - } - - return $context; - } -} diff --git a/src/JsonLd/ContextBuilderInterface.php b/src/JsonLd/ContextBuilderInterface.php deleted file mode 100644 index e4c171e..0000000 --- a/src/JsonLd/ContextBuilderInterface.php +++ /dev/null @@ -1,54 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace Zfegg\ApiSerializerExt\JsonLd; - -use Zfegg\ApiSerializerExt\Api\UrlGeneratorInterface; -use Zfegg\ApiSerializerExt\Exception\ResourceClassNotFoundException; - -/** - * JSON-LD context builder interface. - * - * @author Kévin Dunglas - */ -interface ContextBuilderInterface -{ - public const HYDRA_NS = 'http://www.w3.org/ns/hydra/core#'; - public const RDF_NS = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#'; - public const RDFS_NS = 'http://www.w3.org/2000/01/rdf-schema#'; - public const XML_NS = 'http://www.w3.org/2001/XMLSchema#'; - public const OWL_NS = 'http://www.w3.org/2002/07/owl#'; - public const SCHEMA_ORG_NS = 'http://schema.org/'; - - /** - * Gets the base context. - */ - public function getBaseContext(int $referenceType = UrlGeneratorInterface::ABS_PATH): array; - - /** - * Builds the JSON-LD context for the entrypoint. - */ - public function getEntrypointContext(int $referenceType = UrlGeneratorInterface::ABS_PATH): array; - - /** - * Builds the JSON-LD context for the given resource. - * - * @throws ResourceClassNotFoundException - */ - public function getResourceContext(string $resourceClass, int $referenceType = UrlGeneratorInterface::ABS_PATH): array; - - /** - * Gets the URI of the given resource context. - */ - public function getResourceContextUri(string $resourceClass, int $referenceType = UrlGeneratorInterface::ABS_PATH): string; -} diff --git a/src/JsonLd/ItemNormalizer.php b/src/JsonLd/ItemNormalizer.php deleted file mode 100644 index 46e622b..0000000 --- a/src/JsonLd/ItemNormalizer.php +++ /dev/null @@ -1,96 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace Zfegg\ApiSerializerExt\JsonLd; - -use Zfegg\ApiSerializerExt\ContextTrait; -use Zfegg\ApiSerializerExt\Serializer\AbstractItemNormalizer; -use Zfegg\ApiSerializerExt\Util\ClassInfoTrait; -use Symfony\Component\Serializer\Exception\LogicException; -use Symfony\Component\Serializer\Exception\NotNormalizableValueException; - -/** - * Converts between objects and array including JSON-LD and Hydra metadata. - * - * @author Kévin Dunglas - */ -final class ItemNormalizer extends AbstractItemNormalizer -{ - use ClassInfoTrait; - use ContextTrait; - use JsonLdContextTrait; - - public const FORMAT = 'jsonld'; - - /** - * {@inheritdoc} - */ - public function supportsNormalization($data, $format = null): bool - { - return self::FORMAT === $format && parent::supportsNormalization($data, $format); - } - - /** - * {@inheritdoc} - * - * @throws LogicException - */ - public function normalize($object, $format = null, array $context = []) - { -// $resourceClass = $this->resourceClassResolver->getResourceClass($object, $context['resource_class'] ?? null); -// $context = $this->initContext($resourceClass, $context); -// $iri = $this->iriConverter->getIriFromItem($object); -// $context['iri'] = $iri; - $context['api_normalize'] = true; - -// $metadata = $this->addJsonLdContext($this->contextBuilder, $resourceClass, $context); - - $data = parent::normalize($object, $format, $context); - if (!\is_array($data)) { - return $data; - } - - -// $metadata['@id'] = $iri; -// $metadata['@type'] = $resourceMetadata->getIri() ?: $resourceMetadata->getShortName(); - - return $data; - } - - /** - * {@inheritdoc} - */ - public function supportsDenormalization($data, $type, $format = null): bool - { - return self::FORMAT === $format && parent::supportsDenormalization($data, $type, $format); - } - - /** - * {@inheritdoc} - * - * @throws NotNormalizableValueException - */ - public function denormalize($data, $class, $format = null, array $context = []) - { - // Avoid issues with proxies if we populated the object - if (isset($data['@id']) && !isset($context[self::OBJECT_TO_POPULATE])) { - if (true !== ($context['api_allow_update'] ?? true)) { - throw new NotNormalizableValueException('Update is not allowed for this operation.'); - } - - $context[self::OBJECT_TO_POPULATE] = $this->iriConverter->getItemFromIri($data['@id'], $context + ['fetch_data' => true]); - } - - return parent::denormalize($data, $class, $format, $context); - } -} diff --git a/src/JsonLd/JsonLdContextTrait.php b/src/JsonLd/JsonLdContextTrait.php deleted file mode 100644 index caa6c24..0000000 --- a/src/JsonLd/JsonLdContextTrait.php +++ /dev/null @@ -1,58 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace Zfegg\ApiSerializerExt\JsonLd; - -/** - * Creates and manipulates the Serializer context. - * - * @author Kévin Dunglas - * - * @internal - */ -trait JsonLdContextTrait -{ - /** - * Updates the given JSON-LD document to add its @context key. - */ - private function addJsonLdContext(ContextBuilderInterface $contextBuilder, string $resourceClass, array &$context, array $data = []): array - { - if (isset($context['jsonld_has_context'])) { - return $data; - } - - $context['jsonld_has_context'] = true; - - if (isset($context['jsonld_embed_context'])) { - $data['@context'] = $contextBuilder->getResourceContext($resourceClass); - - return $data; - } - - $data['@context'] = $contextBuilder->getResourceContextUri($resourceClass); - - return $data; - } - - private function createJsonLdContext(AnonymousContextBuilderInterface $contextBuilder, $object, array &$context, array $data = []): array - { - // We're in a collection, just add the IRI if available - if (isset($context['jsonld_has_context'])) { - return isset($context['output']['iri']) ? ['@id' => $context['output']['iri']] : $data; - } - - $context['jsonld_has_context'] = true; - - return $contextBuilder->getAnonymousResourceContext($object, ($context['output'] ?? []) + ['api_resource' => $context['api_resource'] ?? null]); - } -} diff --git a/src/JsonLd/ObjectNormalizer.php b/src/JsonLd/ObjectNormalizer.php deleted file mode 100644 index 4e27be1..0000000 --- a/src/JsonLd/ObjectNormalizer.php +++ /dev/null @@ -1,94 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace Zfegg\ApiSerializerExt\JsonLd; - -use Zfegg\ApiSerializerExt\Api\IriConverterInterface; -use Zfegg\ApiSerializerExt\Metadata\Property\PropertyMetadata; -use Symfony\Component\Serializer\Normalizer\CacheableSupportsMethodInterface; -use Symfony\Component\Serializer\Normalizer\NormalizerInterface; - -/** - * Decorates the output with JSON-LD metadata when appropriate, but otherwise just - * passes through to the decorated normalizer. - */ -final class ObjectNormalizer implements NormalizerInterface, CacheableSupportsMethodInterface -{ - use JsonLdContextTrait; - - public const FORMAT = 'jsonld'; - - private $decorated; - private $iriConverter; - private $anonymousContextBuilder; - - public function __construct(NormalizerInterface $decorated, IriConverterInterface $iriConverter, AnonymousContextBuilderInterface $anonymousContextBuilder) - { - $this->decorated = $decorated; - $this->iriConverter = $iriConverter; - $this->anonymousContextBuilder = $anonymousContextBuilder; - } - - /** - * {@inheritdoc} - */ - public function supportsNormalization($data, $format = null, array $context = []): bool - { - return self::FORMAT === $format && $this->decorated->supportsNormalization($data, $format, $context); - } - - /** - * {@inheritdoc} - */ - public function hasCacheableSupportsMethod(): bool - { - return $this->decorated instanceof CacheableSupportsMethodInterface && $this->decorated->hasCacheableSupportsMethod(); - } - - /** - * {@inheritdoc} - */ - public function normalize($object, $format = null, array $context = []) - { - if (isset($context['api_resource'])) { - $originalResource = $context['api_resource']; - unset($context['api_resource']); - } - - /* - * Converts the normalized data array of a resource into an IRI, if the - * normalized data array is empty. - * - * This is useful when traversing from a non-resource towards an attribute - * which is a resource, as we do not have the benefit of {@see PropertyMetadata::isReadableLink}. - * - * It must not be propagated to subresources, as {@see PropertyMetadata::isReadableLink} - * should take effect. - */ - $context['api_empty_resource_as_iri'] = true; - - $data = $this->decorated->normalize($object, $format, $context); - if (!\is_array($data)) { - return $data; - } - - if (isset($originalResource)) { - $context['output']['iri'] = $this->iriConverter->getIriFromItem($originalResource); - $context['api_resource'] = $originalResource; - } - - $metadata = $this->createJsonLdContext($this->anonymousContextBuilder, $object, $context); - - return $metadata + $data; - } -} diff --git a/src/Util/CorsTrait.php b/src/Util/CorsTrait.php deleted file mode 100644 index 011a664..0000000 --- a/src/Util/CorsTrait.php +++ /dev/null @@ -1,33 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace Zfegg\ApiSerializerExt\Util; - -use Symfony\Component\HttpFoundation\Request; - -/** - * CORS utils. - * - * To be removed when https://github.com/symfony/symfony/pull/34391 wil be merged. - * - * @internal - * - * @author Kévin Dunglas - */ -trait CorsTrait -{ - public function isPreflightRequest(Request $request): bool - { - return $request->isMethod('OPTIONS') && $request->headers->has('Access-Control-Request-Method'); - } -} diff --git a/src/Util/ErrorFormatGuesser.php b/src/Util/ErrorFormatGuesser.php deleted file mode 100644 index 056cdb4..0000000 --- a/src/Util/ErrorFormatGuesser.php +++ /dev/null @@ -1,55 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace Zfegg\ApiSerializerExt\Util; - -use Symfony\Component\HttpFoundation\Request; - -/** - * Guesses the error format to use. - * - * @author Kévin Dunglas - */ -final class ErrorFormatGuesser -{ - private function __construct() - { - } - - /** - * Get the error format and its associated MIME type. - */ - public static function guessErrorFormat(Request $request, array $errorFormats): array - { - $requestFormat = $request->getRequestFormat(''); - - if ('' !== $requestFormat && isset($errorFormats[$requestFormat])) { - return ['key' => $requestFormat, 'value' => $errorFormats[$requestFormat]]; - } - - $requestMimeTypes = Request::getMimeTypes($request->getRequestFormat()); - $defaultFormat = []; - - foreach ($errorFormats as $format => $errorMimeTypes) { - if (array_intersect($requestMimeTypes, $errorMimeTypes)) { - return ['key' => $format, 'value' => $errorMimeTypes]; - } - - if (!$defaultFormat) { - $defaultFormat = ['key' => $format, 'value' => $errorMimeTypes]; - } - } - - return $defaultFormat; - } -} diff --git a/src/Util/RequestAttributesExtractor.php b/src/Util/RequestAttributesExtractor.php deleted file mode 100644 index 8fbd97b..0000000 --- a/src/Util/RequestAttributesExtractor.php +++ /dev/null @@ -1,37 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace Zfegg\ApiSerializerExt\Util; - -use Symfony\Component\HttpFoundation\Request; - -/** - * Extracts data used by the library form a Request instance. - * - * @author Kévin Dunglas - */ -final class RequestAttributesExtractor -{ - private function __construct() - { - } - - /** - * Extracts resource class, operation name and format request attributes. Returns an empty array if the request does - * not contain required attributes. - */ - public static function extractAttributes(Request $request): array - { - return AttributesExtractor::extractAttributes($request->attributes->all()); - } -} diff --git a/src/Util/RequestParser.php b/src/Util/RequestParser.php deleted file mode 100644 index 118813c..0000000 --- a/src/Util/RequestParser.php +++ /dev/null @@ -1,105 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace Zfegg\ApiSerializerExt\Util; - -use Symfony\Component\HttpFoundation\Request; - -/** - * Utility functions for working with Symfony's HttpFoundation request. - * - * @internal - * - * @author Teoh Han Hui - * @author Vincent Chalamon - */ -final class RequestParser -{ - private function __construct() - { - } - - /** - * Gets a fixed request. - */ - public static function parseAndDuplicateRequest(Request $request): Request - { - $query = self::parseRequestParams(self::getQueryString($request) ?? ''); - $body = self::parseRequestParams($request->getContent()); - - return $request->duplicate($query, $body); - } - - /** - * Parses request parameters from the specified source. - * - * @author Rok Kralj - * - * @see https://stackoverflow.com/a/18209799/1529493 - */ - public static function parseRequestParams(string $source): array - { - // '[' is urlencoded ('%5B') in the input, but we must urldecode it in order - // to find it when replacing names with the regexp below. - $source = str_replace('%5B', '[', $source); - - $source = preg_replace_callback( - '/(^|(?<=&))[^=[&]+/', - function ($key) { - return bin2hex(urldecode($key[0])); - }, - $source - ); - - // parse_str urldecodes both keys and values in resulting array. - parse_str($source, $params); - - return array_combine(array_map('hex2bin', array_keys($params)), $params); - } - - /** - * Generates the normalized query string for the Request. - * - * It builds a normalized query string, where keys/value pairs are alphabetized - * and have consistent escaping. - */ - public static function getQueryString(Request $request): ?string - { - $qs = $request->server->get('QUERY_STRING', ''); - if ('' === $qs) { - return null; - } - - $parts = []; - - foreach (explode('&', $qs) as $param) { - if ('' === $param || '=' === $param[0]) { - // Ignore useless delimiters, e.g. "x=y&". - // Also ignore pairs with empty key, even if there was a value, e.g. "=value", as such nameless values cannot be retrieved anyway. - // PHP also does not include them when building _GET. - continue; - } - - $keyValuePair = explode('=', $param, 2); - - // GET parameters, that are submitted from a HTML form, encode spaces as "+" by default (as defined in enctype application/x-www-form-urlencoded). - // PHP also converts "+" to spaces when filling the global _GET or when using the function parse_str. This is why we use urldecode and then normalize to - // RFC 3986 with rawurlencode. - $parts[] = isset($keyValuePair[1]) ? - rawurlencode(urldecode($keyValuePair[0])).'='.rawurlencode(urldecode($keyValuePair[1])) : - rawurlencode(urldecode($keyValuePair[0])); - } - - return implode('&', $parts); - } -} diff --git a/src/Util/ResourceClassInfoTrait.php b/src/Util/ResourceClassInfoTrait.php deleted file mode 100644 index 9f3d245..0000000 --- a/src/Util/ResourceClassInfoTrait.php +++ /dev/null @@ -1,81 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace Zfegg\ApiSerializerExt\Util; - -use Zfegg\ApiSerializerExt\Api\ResourceClassResolverInterface; -use Zfegg\ApiSerializerExt\Exception\ResourceClassNotFoundException; -use Zfegg\ApiSerializerExt\Metadata\Resource\Factory\ResourceMetadataFactoryInterface; - -/** - * Retrieves information about a resource class. - * - * @internal - */ -trait ResourceClassInfoTrait -{ - use ClassInfoTrait; - - /** - * @var ResourceClassResolverInterface|null - */ - private $resourceClassResolver; - - /** - * @var ResourceMetadataFactoryInterface|null - */ - private $resourceMetadataFactory; - - /** - * Gets the resource class of the given object. - * - * @param object $object - * @param bool $strict If true, object class is expected to be a resource class - * - * @return string|null The resource class, or null if object class is not a resource class - */ - private function getResourceClass($object, bool $strict = false): ?string - { - $objectClass = $this->getObjectClass($object); - - if (null === $this->resourceClassResolver) { - return $objectClass; - } - - if (!$strict && !$this->resourceClassResolver->isResourceClass($objectClass)) { - return null; - } - - return $this->resourceClassResolver->getResourceClass($object); - } - - private function isResourceClass(string $class): bool - { - if ($this->resourceClassResolver instanceof ResourceClassResolverInterface) { - return $this->resourceClassResolver->isResourceClass($class); - } - - if (!$this->resourceMetadataFactory instanceof ResourceMetadataFactoryInterface) { - // assume that it's a resource class - return true; - } - - try { - $this->resourceMetadataFactory->create($class); - } catch (ResourceClassNotFoundException $e) { - return false; - } - - return true; - } -} diff --git a/test/Basic/CollectionNormalizerTest.php b/test/Basic/CollectionNormalizerTest.php index 4fdb51b..7890b81 100644 --- a/test/Basic/CollectionNormalizerTest.php +++ b/test/Basic/CollectionNormalizerTest.php @@ -1,23 +1,27 @@ 1, 'name' => 'aaa'], - ] + ], + 100, 1, 10 ); $serializer = new Serializer( @@ -30,10 +34,11 @@ public function testNormalize() ] ); - $result = $serializer->serialize($data, 'json'); + $context['api_resource'] = 'collection'; + $result = $serializer->serialize($data, 'json', $context); $this->assertJsonStringEqualsJsonString( - '{"total":100,"page":1,"page_size":10,"data":[{"id":1,"name":"aaa"}]}', + '{"total":100,"page":1,"page_count":10,"page_size":10,"data":[{"id":1,"name":"aaa"}]}', $result ); } diff --git a/test/Collection.php b/test/Collection.php index a0c91ea..3fae660 100644 --- a/test/Collection.php +++ b/test/Collection.php @@ -1,8 +1,8 @@ 1, 'name' => 'aaa'], - ]; - - $result = $serializer->serialize($data, 'jsonhal'); - - $this->assertJsonStringEqualsJsonString( - $result, - $result - ); - } -} diff --git a/test/HalTest.php b/test/HalTest.php deleted file mode 100644 index d3df283..0000000 --- a/test/HalTest.php +++ /dev/null @@ -1,20 +0,0 @@ - 1, 'name' => 'aaa'], - ]; - - $result = $serializer->serialize($data, 'jsonld'); - - $this->assertJsonStringEqualsJsonString( - $result, - $result - ); - } -}