An error occurred while loading the file. Please try again.
-
Guillaume Perréal authored63c249b7
<?php declare(strict_types=1);
/*
* irstea/ng-model-generator-bundle generates Typescript interfaces for Angular using api-platform metadata.
* Copyright (C) 2018 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 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 */
private $paginationMetadata;
/** @var SerializationMetadata[] */
7172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
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,
$class,
true,
$metadata->getAttribute('normalization_context', [])['groups'] ?? []
141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210
);
return new ResourceMetadata(
$class,
$parentClass ? PHPClass::get($parentClass->getName()) : null,
$metadata->getShortName(),
$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) {
foreach ($ops as $name => $operation) {
$operations[] = $this->getOperation($class, $resourceMetadata, $name, $type, $operation);
}
}
return $operations;
}
/**
* @param ClassName $class
* @param APIResourceMetadata $resourceMetadata
* @param string $name
* @param string $type
* @param array $operation
*
* @return OperationMetadata
211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280
*/
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);
}
/** @noinspection PhpMethodParametersCountMismatchInspection */
$path = $this->operationPathResolver->resolveOperationPath($resourceMetadata->getShortName(), $operation, $type, $name);
// 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', []));
$pagination = $this->buildPagination(
$getAttribute('pagination_enabled', true),
$getAttribute('pagination_client_items_per_page', true)
);
} else {
$filters = [];
$pagination = null;
}
$opDef = new OperationDef($name, $method, $type === OperationType::COLLECTION);
if ($opDef->hasNormalization()) {
$normalization = $this->getOperationSerialization(
$opDef,
$class,
true,
$getAttribute('normalization_context', [])['groups'] ?? []
);
} else {
$normalization = null;
}
if ($opDef->hasDenormalization()) {
$denormalization = $this->getOperationSerialization(
$opDef,
$class,
false,
$getAttribute('denormalization_context', [])['groups'] ?? []
);
} else {
$denormalization = null;
}
return new OperationMetadata(
$opDef,
$operation['description'] ?? '',
$path,
$getAttribute('requirements', []),
$filters,
$pagination,
$normalization,
$denormalization
);
}
281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350
/**
* @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;
}
/** @var FilterInterface $filter */
$filter = $this->filterLocator->get($filterId);
foreach ($filter->getDescription($class->getFullName()) as $name => $filterParameter) {
$filters[$name] = $filterParameter;
}
}
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])) {
$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
{
351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420
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->propertyNameCollectionFactory,
$this->propertyMetadataFactory,
$class,
$mode,
$groups
);
/** @var RepresentationMetadata[] $reprs */
$representations = [];
/** @var ClassName[] $queue */
$queue = [$class];
while ($queue) {
$current = array_shift($queue);
if (!$this->isResource($current) || 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();
if ($type->getClassName()) {
$queue[] = PHPClass::get($type->getClassName());
}
}
}
$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
*
421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471
* @throws \ApiPlatform\Core\Exception\ResourceClassNotFoundException
*
* @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);
$this->defaultGroups[$className] = $groups;
return $groups;
}
}