MetadataFactory.php 17.04 KiB
<?php declare(strict_types=1);
/*
 * This file is part of "irstea/ng-model-generator-bundle".
 * "irstea/ng-model-generator-bundle" generates Typescript interfaces for Angular using api-platform metadata.
 * Copyright (C) 2018-2019 IRSTEA
 * This program is free software: you can redistribute it and/or modify it under
 * the terms of the GNU Lesser General Public License as published by the Free
 * Software Foundation, either version 3 of the License, or (at your option) any
 * later version.
 * This program is distributed in the hope that it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
 * PARTICULAR PURPOSE. See the GNU General Public License for more details.
 * You should have received a copy of the GNU General Public License and the GNU
 * Lesser General Public License along with this program. If not, see
 * <https://www.gnu.org/licenses/>.
namespace Irstea\NgModelGeneratorBundle\Metadata;
use ApiPlatform\Core\Api\FilterInterface;
use ApiPlatform\Core\Api\OperationMethodResolverInterface;
use ApiPlatform\Core\Api\OperationType;
use ApiPlatform\Core\Api\ResourceClassResolverInterface;
use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
use ApiPlatform\Core\Metadata\Resource\ResourceMetadata as APIResourceMetadata;
use ApiPlatform\Core\PathResolver\OperationPathResolverInterface;
use Assert\Assertion;
use Doctrine\Common\Inflector\Inflector;
use Irstea\NgModelGeneratorBundle\Models\ClassName;
use Irstea\NgModelGeneratorBundle\Models\PHPClass;
use Psr\Container\ContainerInterface;
use Symfony\Component\PropertyInfo\PropertyInfoExtractorInterface;
/**
 * Class MetadataFactory
 * Cette classe est chargée de collecter les métadonnées des différents services d'API-Platform.
final class MetadataFactory implements MetadataFactoryInterface
    /** @var ResourceClassResolverInterface */
    private $resourceClassResolver;
    /** @var ResourceMetadataFactoryInterface */
    private $resourceMetadataFactory;
    /** @var PropertyNameCollectionFactoryInterface */
    private $propertyNameCollectionFactory;
    /** @var PropertyMetadataFactoryInterface */
    private $propertyMetadataFactory;
    /** @var PropertyInfoExtractorInterface */
    private $propertyInfoExtractor;
    /** @var OperationMethodResolverInterface */
    private $operationMethodResolver;
    /** @var OperationPathResolverInterface */
    private $operationPathResolver;
    /** @var ContainerInterface */
    private $filterLocator;
    /** @var PaginationMetadata */
7172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
private $paginationMetadata; /** @var SerializationMetadata[] */ private $serializations = []; /** @var ClassHierarchy */ private $classHierarchy; /** @var string[][] */ private $defaultGroups = []; /** * MetadataFactory constructor. * * @param ResourceClassResolverInterface $resourceClassResolver * @param ResourceMetadataFactoryInterface $resourceMetadataFactory * @param PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory * @param PropertyMetadataFactoryInterface $propertyMetadataFactory * @param PropertyInfoExtractorInterface $propertyInfoExtractor * @param OperationMethodResolverInterface $operationMethodResolver * @param OperationPathResolverInterface $operationPathResolver * @param ContainerInterface $filterLocator * @param PaginationMetadata $paginationMetadata * @param ClassHierarchy $classHierarchy */ public function __construct( ResourceClassResolverInterface $resourceClassResolver, ResourceMetadataFactoryInterface $resourceMetadataFactory, PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, PropertyInfoExtractorInterface $propertyInfoExtractor, OperationMethodResolverInterface $operationMethodResolver, OperationPathResolverInterface $operationPathResolver, ContainerInterface $filterLocator, PaginationMetadata $paginationMetadata, ClassHierarchy $classHierarchy ) { $this->resourceClassResolver = $resourceClassResolver; $this->resourceMetadataFactory = $resourceMetadataFactory; $this->propertyNameCollectionFactory = $propertyNameCollectionFactory; $this->propertyMetadataFactory = $propertyMetadataFactory; $this->propertyInfoExtractor = $propertyInfoExtractor; $this->operationMethodResolver = $operationMethodResolver; $this->operationPathResolver = $operationPathResolver; $this->filterLocator = $filterLocator; $this->paginationMetadata = $paginationMetadata; $this->classHierarchy = $classHierarchy; } /** * {@inheritdoc} */ public function isResource(ClassName $class): bool { return $this->resourceClassResolver->isResourceClass($class->getFullName()); } /** * {@inheritdoc} */ public function getResourceMetadata(ClassName $class): ResourceMetadata { $className = $class->getFullName(); $metadata = $this->resourceMetadataFactory->create($className); $classMeta = new \ReflectionClass($className); $parentClass = $classMeta->getParentClass(); $defaultNormalization = $this->getOperationSerialization( null,
141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210
$class, true, $metadata->getAttribute('normalization_context', [])['groups'] ?? [] ); return new ResourceMetadata( $class, $parentClass ? PHPClass::get($parentClass->getName()) : null, $metadata->getDescription() ?: '', $classMeta->isAbstract(), $defaultNormalization, $this->getOperations($class) ); } /** * @param bool $enabled * @param bool $clientItemsPerPage * * @return PaginationMetadata */ private function buildPagination(bool $enabled, bool $clientItemsPerPage): PaginationMetadata { return new PaginationMetadata( $enabled && $this->paginationMetadata->isEnabled(), $this->paginationMetadata->getPageParameterName(), $clientItemsPerPage && $this->paginationMetadata->isClientItemsPerPage(), $this->paginationMetadata->getItemsPerPageParameterName() ); } /** * Get paginationMetadata. * * @return PaginationMetadata */ public function getPaginationMetadata(): PaginationMetadata { return $this->paginationMetadata; } /** * @param ClassName $class * * @return OperationMetadata[] */ private function getOperations(ClassName $class): array { $resourceMetadata = $this->resourceMetadataFactory->create($class->getFullName()); $operations = []; foreach ([ OperationType::ITEM => $resourceMetadata->getItemOperations(), OperationType::COLLECTION => $resourceMetadata->getCollectionOperations(), ] as $type => $ops) { if (!$ops) { continue; } foreach ($ops as $name => $operation) { $operations[] = $this->getOperation($class, $resourceMetadata, (string) $name, $type, $operation); } } return $operations; } /** * @param ClassName $class * @param APIResourceMetadata $resourceMetadata
211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280
* @param string $name * @param string $type * @param array $operation * * @return OperationMetadata */ private function getOperation( ClassName $class, APIResourceMetadata $resourceMetadata, string $name, string $type, array $operation ): OperationMetadata { if ($type === OperationType::ITEM) { $method = $this->operationMethodResolver->getItemOperationMethod($class->getFullName(), $name); } else { $method = $this->operationMethodResolver->getCollectionOperationMethod($class->getFullName(), $name); } $shortName = $resourceMetadata->getShortName(); Assertion::notNull($shortName); /** @noinspection PhpMethodParametersCountMismatchInspection */ $path = $this->operationPathResolver->resolveOperationPath($shortName, $operation, $type); // Suppression du suffixe de format, qui n'est pas utilisable en dehors de Symfony $path = \str_replace('.{_format}', '', $path); $getAttribute = function (string $attrName, $default) use ($resourceMetadata, $type, $name) { return $resourceMetadata->getTypedOperationAttribute($type, $name, $attrName, $default, true); }; if ($type === OperationType::COLLECTION && $method === 'GET') { $filters = $this->getFilters($class, $getAttribute('filters', [])); $paginationEnabled = (bool) $getAttribute('pagination_enabled', true); $paginationClientItemsPerPage = (bool) $getAttribute('pagination_client_items_per_page', true); $pagination = $this->buildPagination($paginationEnabled, $paginationClientItemsPerPage); } else { $filters = []; $pagination = null; } $opDef = new OperationDef($name, $method, $type === OperationType::COLLECTION); $output = $getAttribute('output', null); $normalization = null; if ($output !== false) { if (is_array($output) && isset($output['class'])) { $output = $output['class']; } if ($output === null && $opDef->hasNormalization()) { $output = $class->getFullName(); } if ($output) { $normalization = $this->getOperationSerialization( $opDef, PHPClass::get($output), true, $getAttribute('normalization_context', [])['groups'] ?? [] ); } } $input = $getAttribute('input', null); $denormalization = null; if ($input !== false) { if (is_array($input) && isset($input['class'])) { $input = $input['class']; }
281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350
if ($input === null && $opDef->hasDenormalization()) { $input = $class->getFullName(); } if ($input) { $denormalization = $this->getOperationSerialization( $opDef, PHPClass::get($input), false, $getAttribute('denormalization_context', [])['groups'] ?? [] ); } } return new OperationMetadata( $opDef, $operation['description'] ?? '', $path, $getAttribute('requirements', []), $filters, $pagination, $normalization, $denormalization ); } /** * @param ClassName $class * @param array $filterIds * * @return FilterInterface[] */ private function getFilters(ClassName $class, array $filterIds): array { $filters = []; foreach ($filterIds as $filterId) { if (!$this->filterLocator->has($filterId)) { continue; } $filters[] = $this->filterLocator->get($filterId); } return $filters; } /** * @parma string $operationName * * @param OperationDef|null $opDef * @param ClassName $class * @param bool $normalization * @param string[] $groups * * @throws \ApiPlatform\Core\Exception\PropertyNotFoundException * @throws \ApiPlatform\Core\Exception\ResourceClassNotFoundException * @throws \ReflectionException * * @return SerializationMetadata */ private function getOperationSerialization( ?OperationDef $opDef, ClassName $class, bool $normalization, array $groups ): SerializationMetadata { sort($groups); $key = sprintf('%s:%d:%s:%s', $class->getFullName(), $normalization, $opDef ? $opDef->getName() : '_Default_', implode('+', $groups)); if (!isset($this->serializations[$key])) {
351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420
$this->serializations[$key] = $this->doGetSerialization($class, $normalization, $opDef, $groups); } return $this->serializations[$key]; } /** * @param ClassName $class * @param bool $normalization * @param OperationDef $opDef * @param string[] $groups * * @throws \ApiPlatform\Core\Exception\PropertyNotFoundException * @throws \ApiPlatform\Core\Exception\ResourceClassNotFoundException * @throws \ReflectionException * * @return SerializationMetadata */ private function doGetSerialization(ClassName $class, bool $normalization, ?OperationDef $opDef, array $groups): SerializationMetadata { if ($normalization) { $mode = PropertyMetadataFactory::MODE_READ; } elseif ($opDef && $opDef->isCreateItem()) { $mode = PropertyMetadataFactory::MODE_CREATE; } elseif ($opDef && $opDef->isUpdateItem()) { $mode = PropertyMetadataFactory::MODE_UPDATE; } else { $mode = PropertyMetadataFactory::MODE_OTHER; } $propertyMetadataFactory = new PropertyMetadataFactory( $this->resourceClassResolver, $this->propertyNameCollectionFactory, $this->propertyMetadataFactory, $class, $mode, $groups ); /** @var RepresentationMetadata[] $reprs */ $representations = []; /** @var ClassName[] $queue */ $queue = [$class]; while ($queue) { $current = array_shift($queue); if (isset($representations[$current->getFullName()])) { continue; } $parent = $this->classHierarchy->getParent($current); if ($parent) { $queue[] = $parent; } foreach ($this->classHierarchy->getChildren($current) as $children) { $queue[] = $children; } $propertiesMeta = $propertyMetadataFactory->getAPIMetadata($current); $properties = []; foreach ($propertiesMeta as $propertyName => $propertyMeta) { $property = $propertyMetadataFactory->create($current, $propertyName); $properties[$propertyName] = $property; if ($property->isEmbedded()) { $type = $property->getLeafType(); $className = $type->getClassName(); if ($className) {
421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490
$queue[] = PHPClass::get($className); } } } $name = $this->getRepresentationName($class, $current, $normalization, $opDef, $groups); $abstract = (new \ReflectionClass($current->getFullName()))->isAbstract(); $representations[$current->getFullName()] = new RepresentationMetadata($name, $current, $parent, $properties, $abstract); } return new SerializationMetadata($class, $groups, $normalization, $representations); } /** * @param ClassName $root * @param ClassName $class * @param bool $normalization * @param OperationDef|null $opDef * @param array $groups * * @return string */ private function getRepresentationName( ClassName $root, ClassName $class, bool $normalization, ?OperationDef $opDef, array $groups ): string { if ($normalization && (!$groups || $groups === $this->getDefaultGroups($class))) { return $class->getBaseName(); } if ($opDef) { $name = $opDef->getOriginalName(); } elseif ($groups) { $name = implode('', $groups); } else { $name = $normalization ? 'Read' : 'Write'; } if (strpos($name, $root->getBaseName()) === false) { $name .= $root->getBaseName(); } if (strpos($name, $class->getBaseName()) === false) { $name .= $class->getBaseName(); } return Inflector::classify($name); } /** * @param ClassName $class * * @return array */ private function getDefaultGroups(ClassName $class): array { $className = $class->getFullName(); if (isset($this->defaultGroups[$className])) { return $this->defaultGroups[$className]; } $context = $this->resourceMetadataFactory->create($className)->getAttribute('normalization_context', []); $groups = $context['groups'] ?? []; sort($groups);
491492493494495496
$this->defaultGroups[$className] = $groups; return $groups; } }