diff --git a/src/Iterators/UniqueFilter.php b/src/Iterators/UniqueFilter.php index 60b35356c8d775bf5b83d638277de43fcb0f723c..a750b8c2b9ba1bbb5daaeb1f816ecc0790e510f3 100644 --- a/src/Iterators/UniqueFilter.php +++ b/src/Iterators/UniqueFilter.php @@ -36,7 +36,7 @@ final class UniqueFilter */ public function __invoke($value): bool { - $key = is_object($value) ? spl_object_hash($value) : (string) $value; + $key = \is_object($value) ? \spl_object_hash($value) : (string) $value; $alreadySeen = isset($this->seen[$key]); $this->seen[$key] = true; diff --git a/src/Metadata/MetadataFactory.php b/src/Metadata/MetadataFactory.php index 40f7ef7bfa3b8ccd0fa1ee46e4e12350359d5092..c2548587aac22d4e90c663096a7d5387d4f84f37 100644 --- a/src/Metadata/MetadataFactory.php +++ b/src/Metadata/MetadataFactory.php @@ -25,9 +25,11 @@ 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\Property\PropertyMetadata as APIPropertyMetadata; 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\PHPClass; use Psr\Container\ContainerInterface; use Symfony\Component\PropertyInfo\PropertyInfoExtractorInterface; @@ -68,6 +70,11 @@ final class MetadataFactory implements MetadataFactoryInterface /** @var SerializationMetadata[] */ private $serializations = []; + /** + * @var ClassHierarchy + */ + private $classHierarchy; + /** * MetadataFactory constructor. * @@ -80,6 +87,7 @@ final class MetadataFactory implements MetadataFactoryInterface * @param OperationPathResolverInterface $operationPathResolver * @param ContainerInterface $filterLocator * @param PaginationMetadata $paginationMetadata + * @param ClassHierarchy $classHierarchy */ public function __construct( ResourceClassResolverInterface $resourceClassResolver, @@ -90,7 +98,8 @@ final class MetadataFactory implements MetadataFactoryInterface OperationMethodResolverInterface $operationMethodResolver, OperationPathResolverInterface $operationPathResolver, ContainerInterface $filterLocator, - PaginationMetadata $paginationMetadata + PaginationMetadata $paginationMetadata, + ClassHierarchy $classHierarchy ) { $this->resourceClassResolver = $resourceClassResolver; $this->resourceMetadataFactory = $resourceMetadataFactory; @@ -101,6 +110,7 @@ final class MetadataFactory implements MetadataFactoryInterface $this->operationPathResolver = $operationPathResolver; $this->filterLocator = $filterLocator; $this->paginationMetadata = $paginationMetadata; + $this->classHierarchy = $classHierarchy; } /** @@ -128,16 +138,6 @@ final class MetadataFactory implements MetadataFactoryInterface $metadata->getShortName(), $metadata->getDescription(), $classMeta->isAbstract(), - $this->getSerialization( - $class, - true, - $metadata->getAttribute('normalization_context', [])['groups'] ?? [] - ), - $this->getSerialization( - $class, - false, - $metadata->getAttribute('normalization_context', [])['groups'] ?? [] - ), $this->getOperations($class) ); } @@ -158,137 +158,6 @@ final class MetadataFactory implements MetadataFactoryInterface ); } - /** - * @param PHPClass $class - * @param bool $normalization - * @param string[] $groups - * - * @return SerializationMetadata - */ - private function getSerialization(PHPClass $class, bool $normalization, array $groups): SerializationMetadata - { - sort($groups); - $key = sprintf('%s:%d:%s', $class->getFullName(), $normalization, implode('+', $groups)); - if (!isset($this->serializations[$key])) { - $this->serializations[$key] = $this->doGetSerialization($class, $normalization, $groups); - } - - return $this->serializations[$key]; - } - - /** - * @param PHPClass $class - * @param bool $normalization - * @param string[] $groups - * - * @return SerializationMetadata - */ - private function doGetSerialization(PHPClass $class, bool $normalization, array $groups): SerializationMetadata - { - /** @var RepresentationMetadata[] $reprs */ - $representations = []; - - /** @var string[] $queue */ - $queue = [$class->getFullName()]; - - while ($queue) { - /** @var string $currentName */ - $currentName = array_shift($queue); - $current = PHPClass::get($currentName); - - if (isset($representations[$currentName]) || !$this->isResource($current)) { - continue; - } - - $parent = null; - $reflParent = $current->getReflection()->getParentClass(); - if ($reflParent) { - $parent = PHPClass::get($reflParent->getName()); - $queue[] = $reflParent->getName(); - } - - $properties = $this->getProperties($current, $normalization, $groups); - foreach ($properties as $property) { - if ($property->getType()->getClassName()) { - $queue[] = $property->getType()->getClassName(); - } - } - - $representations[$currentName] = new RepresentationMetadata($current, $parent, $properties); - } - - return new SerializationMetadata($class, $groups, $normalization, $representations); - } - - /** - * @param PHPClass $class - * @param bool $normalization - * @param array $groups - * - * @throws \ApiPlatform\Core\Exception\PropertyNotFoundException - * @throws \ApiPlatform\Core\Exception\ResourceClassNotFoundException - * - * @return PropertyMetadata[] - */ - private function getProperties(PHPClass $class, bool $normalization, array $groups): array - { - $className = $class->getFullName(); - $properties = []; - - $options = $groups ? ['serializer_groups' => $groups] : []; - - foreach ($this->propertyNameCollectionFactory->create($className, $options) as $propertyName) { - \assert(\is_string($propertyName)); - - $propertyMeta = $this->propertyMetadataFactory->create($className, $propertyName); - - if ($propertyMeta->isChildInherited()) { - continue; - } - - $typeMeta = $propertyMeta->getType(); - if (!$typeMeta) { - continue; - } - - $readable = $propertyMeta->isReadable() && $this->propertyInfoExtractor->isReadable($className, $propertyName); - $writable = ($propertyMeta->isWritable() && $this->propertyInfoExtractor->isWritable($className, $propertyName)); - $initializable = (bool) $propertyMeta->isInitializable(); - $identifier = (bool) $propertyMeta->isIdentifier(); - - if ($normalization ? !$readable : !($identifier || $writable || $initializable)) { - continue; - } - - $link = false; - $embedded = true; - - $leafType = $typeMeta; - while ($leafType && $leafType->isCollection()) { - $leafType = $leafType->getCollectionValueType(); - } - $propClass = $leafType ? $leafType->getClassName() : null; - if ($propClass && $this->isResource(PHPClass::get($propClass))) { - $link = true; - $embedded = \count($this->propertyNameCollectionFactory->create($propClass, $options)) > 0; - } - - $properties[$propertyName] = new PropertyMetadata( - $propertyName, - $this->propertyInfoExtractor->getShortDescription($className, $propertyName) ?: '', - $typeMeta, - $identifier, - $readable, - $writable, - $initializable, - $link, - $embedded - ); - } - - return $properties; - } - /** * Get paginationMetadata. * @@ -365,8 +234,11 @@ final class MetadataFactory implements MetadataFactoryInterface $pagination = null; } - if (\in_array($method, ['GET', 'PUT', 'POST'], true)) { - $normalization = $this->getSerialization( + $opDef = new OperationDef($name, $method, $type === OperationType::COLLECTION); + + if ($opDef->hasNormalization()) { + $normalization = $this->getOperationSerialization( + $opDef, $class, true, $getAttribute('normalization_context', [])['groups'] ?? [] @@ -375,8 +247,9 @@ final class MetadataFactory implements MetadataFactoryInterface $normalization = null; } - if (\in_array($method, ['POST', 'PUT'], true)) { - $denormalization = $this->getSerialization( + if ($opDef->hasDenormalization()) { + $denormalization = $this->getOperationSerialization( + $opDef, $class, false, $getAttribute('denormalization_context', [])['groups'] ?? [] @@ -386,10 +259,8 @@ final class MetadataFactory implements MetadataFactoryInterface } return new OperationMetadata( - $name, + $opDef, $operation['description'] ?? '', - $type, - $method, $path, $getAttribute('requirements', []), $filters, @@ -423,4 +294,221 @@ final class MetadataFactory implements MetadataFactoryInterface return $filters; } + + /** + * @parma string $operationName + * + * @param PHPClass $class + * @param bool $normalization + * @param string[] $groups + * + * @return SerializationMetadata + */ + private function getOperationSerialization(OperationDef $opDef, PHPClass $class, bool $normalization, array $groups): SerializationMetadata + { + sort($groups); + $key = sprintf('%s:%d:%s:%s', $class->getFullName(), $normalization, $opDef->getName(), implode('+', $groups)); + if (!isset($this->serializations[$key])) { + $this->serializations[$key] = $this->doGetSerialization($class, $normalization, $opDef, $groups); + } + + return $this->serializations[$key]; + } + + /** + * @param PHPClass $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(PHPClass $class, bool $normalization, OperationDef $opDef, array $groups): SerializationMetadata + { + $selfNamePrefix = Inflector::classify($opDef->getName()); + $otherNamePrefix = $selfNamePrefix . $class->getBaseName(); + + if ($normalization) { + $metadata = $this->resourceMetadataFactory->create($class->getFullName()); + $defaultGroups = $metadata->getAttribute('normalization_context', [])['groups'] ?? []; + sort($defaultGroups); + if ($defaultGroups === $groups) { + $selfNamePrefix = ''; + } + $propFilter = 'filterGetProperty'; + } elseif ($opDef->isCreateItem()) { + $propFilter = 'filterCreateProperty'; + } elseif ($opDef->isUpdateItem()) { + $propFilter = 'filterUpdateProperty'; + } else { + $propFilter = 'filterAnyProperty'; + } + + /** @var RepresentationMetadata[] $reprs */ + $representations = []; + + /** @var string[] $queue */ + $queue = [$class->getFullName()]; + + while ($queue) { + /** @var string $currentName */ + $currentName = array_shift($queue); + $current = PHPClass::get($currentName); + + if (isset($representations[$currentName]) || !$this->isResource($current)) { + continue; + } + + $parent = $this->classHierarchy->getParent($current); + if ($parent) { + $queue[] = $parent->getFullName(); + } + + foreach ($this->classHierarchy->getChildren($current) as $children) { + $queue[] = $children->getFullName(); + } + + $propertiesMeta = $this->getPropertiesMeta($current, $groups); + + $properties = []; + foreach ($propertiesMeta as $propertyName => $propertyMeta) { + if (!$this->$propFilter($class, $propertyName, $propertyMeta)) { + continue; + } + $property = $this->mapProperty($current, $propertyName, $propertyMeta); + $properties[$propertyName] = $property; + $type = $property->getLeafType(); + if ($type->getClassName()) { + $queue[] = $type->getClassName(); + } + } + + $name = ($current->is($class) ? $selfNamePrefix : $otherNamePrefix) . $current->getBaseName(); + + $representations[$currentName] = new RepresentationMetadata($name, $current, $parent, $properties); + } + + return new SerializationMetadata($class, $groups, $normalization, $representations); + } + + /** + * @param PHPClass $class + * @param string $name + * @param APIPropertyMetadata $property + * + * @return bool + * + * @internal + */ + public function filterGetProperty(PHPClass $class, string $name, APIPropertyMetadata $property): bool + { + return ($property->isReadable() || $property->isReadableLink()) && $this->propertyInfoExtractor->isReadable($class->getFullName(), $name); + } + + /** + * @param PHPClass $class + * @param string $name + * @param APIPropertyMetadata $property + * + * @return bool + * + * @internal + */ + public function filterCreateProperty(PHPClass $class, string $name, APIPropertyMetadata $property): bool + { + return $this->filterUpdateProperty($class, $name, $property) || ($property->isInitializable() ?: false); + } + + /** + * @param PHPClass $class + * @param string $name + * @param APIPropertyMetadata $property + * + * @return bool + * + * @internal + */ + public function filterUpdateProperty(PHPClass $class, string $name, APIPropertyMetadata $property): bool + { + return ($property->isWritable() || $property->isWritableLink()) && $this->propertyInfoExtractor->isWritable($class->getFullName(), $name); + } + + /** + * @param PHPClass $class + * @param string $name + * @param APIPropertyMetadata $property + * + * @return bool + * + * @internal + */ + public function filterAnyProperty(PHPClass $class, string $name, APIPropertyMetadata $property): bool + { + return true; + } + + /** + * @param PHPClass $class + * @param array $groups + * + * @throws \ApiPlatform\Core\Exception\PropertyNotFoundException + * @throws \ApiPlatform\Core\Exception\ResourceClassNotFoundException + * + * @return APIPropertyMetadata[] + */ + private function getPropertiesMeta(PHPClass $class, array $groups): array + { + $properties = []; + $options = $groups ? ['serializer_groups' => $groups] : []; + + foreach ($this->propertyNameCollectionFactory->create($class->getFullName(), $options) as $propertyName) { + \assert(\is_string($propertyName)); + + $propertyMeta = $this->propertyMetadataFactory->create($class->getFullName(), $propertyName); + + if (!$propertyMeta->getType() || $propertyMeta->isChildInherited()) { + continue; + } + + $properties[$propertyName] = $propertyMeta; + } + + return $properties; + } + + /** + * @param PHPClass $class + * @param string $propertyName + * @param APIPropertyMetadata $propertyMeta + * + * @return PropertyMetadata + */ + private function mapProperty(PHPClass $class, string $propertyName, APIPropertyMetadata $propertyMeta): PropertyMetadata + { + $leafType = $typeMeta = $propertyMeta->getType(); + + while ($leafType && $leafType->isCollection()) { + $leafType = $leafType->getCollectionValueType(); + } + $leafClass = $leafType ? $leafType->getClassName() : null; + $link = $leafClass && $this->isResource(PHPClass::get($leafClass)); + + $isReadable = ($propertyMeta->isReadable() || $propertyMeta->isReadableLink()) && $this->propertyInfoExtractor->isReadable($class->getFullName(), $propertyName); + $isWritable = ($propertyMeta->isWritable() || $propertyMeta->isWritableLink()) && $this->propertyInfoExtractor->isWritable($class->getFullName(), $propertyName); + + return new PropertyMetadata( + $propertyName, + $this->propertyInfoExtractor->getShortDescription($class->getFullName(), $propertyName) ?: '', + $typeMeta, + $propertyMeta->isIdentifier(), + $isReadable, + $isWritable, + $propertyMeta->isInitializable() ?: false, + $link + ); + } } diff --git a/src/Metadata/OperationDef.php b/src/Metadata/OperationDef.php new file mode 100644 index 0000000000000000000000000000000000000000..1657976910531ec6204816657b05eda1baef10c2 --- /dev/null +++ b/src/Metadata/OperationDef.php @@ -0,0 +1,178 @@ +<?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 Doctrine\Common\Inflector\Inflector; + +/** + * Class OperationDef. + */ +final class OperationDef +{ + /** @var string */ + private $name; + + /** @var string */ + private $originalName; + + /** @var string */ + private $method; + + /** @var bool */ + private $isCollection; + + /** @var string|null */ + private $special; + + /** + * OperationDef constructor. + * + * @param string $name + * @param string $method + * @param bool $isCollection + */ + public function __construct(string $name, string $method, bool $isCollection) + { + $this->originalName = $name; + $this->method = $method; + + if (\strtolower($method) === \strtolower($name)) { + $this->special = \strtoupper($method); + if ($method === 'POST') { + $isCollection = false; + } + } + $this->isCollection = $isCollection; + + $name = Inflector::camelize($name); + + if ($method === 'GET' && strpos($name, 'get') === false) { + $name = 'get' . ucfirst($name); + } + + if ($isCollection) { + if (\in_array($name, ['get', 'put', 'delete', 'patch'], true)) { + $name .= 'All'; + } else { + $name = Inflector::pluralize($name); + } + } else { + $name = Inflector::singularize($name); + } + + $this->name = $name; + } + + /** + * @return bool + */ + public function hasNormalization(): bool + { + return \in_array($this->method, ['GET', 'PUT', 'POST']); + } + + /** + * @return bool + */ + public function hasDenormalization(): bool + { + return \in_array($this->method, ['PUT', 'POST']); + } + + /** + * @return bool + */ + public function isGetItem(): bool + { + return $this->special === 'GET' && !$this->isCollection; + } + + /** + * @return bool + */ + public function isCreateItem(): bool + { + return $this->special === 'POST' && !$this->isCollection; + } + + /** + * @return bool + */ + public function isUpdateItem(): bool + { + return $this->special === 'PUT' && !$this->isCollection; + } + + /** + * @return bool + */ + public function isDeleteItem(): bool + { + return $this->special === 'DELETE' && !$this->isCollection; + } + + /** + * @return bool + */ + public function isGetCollection(): bool + { + return $this->method === 'GET' && $this->isCollection; + } + + /** + * Get name. + * + * @return string + */ + public function getName(): string + { + return $this->name; + } + + /** + * Get originalName. + * + * @return string + */ + public function getOriginalName(): string + { + return $this->originalName; + } + + /** + * Get method. + * + * @return string + */ + public function getMethod(): string + { + return $this->method; + } + + /** + * Get isCollection. + * + * @return bool + */ + public function isCollection(): bool + { + return $this->isCollection; + } +} diff --git a/src/Metadata/OperationMetadata.php b/src/Metadata/OperationMetadata.php index 63a0ec8dacf0565e7a45df0dc45785c133783180..4924b70bf015a9d23fd1ef3a58f884451d141ee6 100644 --- a/src/Metadata/OperationMetadata.php +++ b/src/Metadata/OperationMetadata.php @@ -20,25 +20,18 @@ namespace Irstea\NgModelGeneratorBundle\Metadata; use ApiPlatform\Core\Api\FilterInterface; -use ApiPlatform\Core\Api\OperationType; /** * Class OperationMetadata. */ class OperationMetadata implements \JsonSerializable { - /** @var string */ - private $name; + /** @var OperationDef */ + private $opDef; /** @var string */ private $description; - /** @var string */ - private $type; - - /** @var string */ - private $method; - /** @var string */ private $path; @@ -63,10 +56,8 @@ class OperationMetadata implements \JsonSerializable /** * OperationMetadata constructor. * - * @param string $name + * @param OperationDef $opDef * @param string $description - * @param string $type - * @param string $method * @param string $path * @param array $requirements * @param array $filters @@ -75,10 +66,8 @@ class OperationMetadata implements \JsonSerializable * @param SerializationMetadata|null $denormalization */ public function __construct( - string $name, + OperationDef $opDef, string $description, - string $type, - string $method, string $path, array $requirements, array $filters, @@ -86,9 +75,6 @@ class OperationMetadata implements \JsonSerializable ?SerializationMetadata $normalization, ?SerializationMetadata $denormalization ) { - $this->name = $name; - $this->type = $type; - $this->method = $method; $this->path = $path; $this->filters = $filters; $this->description = $description; @@ -96,6 +82,7 @@ class OperationMetadata implements \JsonSerializable $this->requirements = $requirements; $this->normalization = $normalization; $this->denormalization = $denormalization; + $this->opDef = $opDef; } /** @@ -121,6 +108,16 @@ class OperationMetadata implements \JsonSerializable return $this->resource->getClassName(); } + /** + * Get opDef. + * + * @return OperationDef + */ + public function getOpDef(): OperationDef + { + return $this->opDef; + } + /** * Get name. * @@ -128,7 +125,7 @@ class OperationMetadata implements \JsonSerializable */ public function getName(): string { - return $this->name; + return $this->opDef->getName(); } /** @@ -148,7 +145,7 @@ class OperationMetadata implements \JsonSerializable */ public function getType(): string { - return $this->type; + return $this->opDef->isCollection() ? 'collection' : 'item'; } /** @@ -156,7 +153,7 @@ class OperationMetadata implements \JsonSerializable */ public function isItemOperation(): bool { - return $this->type === OperationType::ITEM; + return !$this->opDef->isCollection(); } /** @@ -164,7 +161,7 @@ class OperationMetadata implements \JsonSerializable */ public function isCollectionOperation(): bool { - return $this->type === OperationType::COLLECTION; + return $this->opDef->isCollection(); } /** @@ -174,7 +171,7 @@ class OperationMetadata implements \JsonSerializable */ public function getMethod(): string { - return $this->method; + return $this->opDef->getMethod(); } /** diff --git a/src/Metadata/PropertyMetadata.php b/src/Metadata/PropertyMetadata.php index 60d7c361acc6a75d287f795846f2a95bb54073ec..eb205f4d5250aba2f6e0fffe666dfb2bb304ffc1 100644 --- a/src/Metadata/PropertyMetadata.php +++ b/src/Metadata/PropertyMetadata.php @@ -49,9 +49,6 @@ class PropertyMetadata implements \JsonSerializable /** @var bool */ private $link; - /** @var bool */ - private $embedded; - /** * PropertyMetadata constructor. * @@ -63,7 +60,6 @@ class PropertyMetadata implements \JsonSerializable * @param bool $writable * @param bool $initializable * @param bool $link - * @param bool $embedded */ public function __construct( string $name, @@ -73,8 +69,7 @@ class PropertyMetadata implements \JsonSerializable bool $readable, bool $writable, bool $initializable, - bool $link, - bool $embedded + bool $link ) { $this->name = $name; $this->description = $description; @@ -84,7 +79,6 @@ class PropertyMetadata implements \JsonSerializable $this->writable = $writable; $this->initializable = $initializable; $this->link = $link; - $this->embedded = $embedded; } /** @@ -188,16 +182,6 @@ class PropertyMetadata implements \JsonSerializable return $this->link; } - /** - * Get embedded. - * - * @return bool - */ - public function isEmbedded(): bool - { - return $this->embedded; - } - /** * {@inheritdoc} */ diff --git a/src/Metadata/RepresentationMetadata.php b/src/Metadata/RepresentationMetadata.php index 58f61bf3ef8fe4f65f8d099d1c60e51d9e7fbebd..6145b37550eba34822b212dc07a452c0b67dd62c 100644 --- a/src/Metadata/RepresentationMetadata.php +++ b/src/Metadata/RepresentationMetadata.php @@ -26,6 +26,11 @@ use Irstea\NgModelGeneratorBundle\Models\PHPClass; */ final class RepresentationMetadata implements \JsonSerializable { + /** + * @var string + */ + private $name; + /** * @var PHPClass */ @@ -44,11 +49,12 @@ final class RepresentationMetadata implements \JsonSerializable /** * RepresentationMetadata constructor. * + * @param string $name * @param PHPClass $class * @param PHPClass|null $parent * @param PropertyMetadata[] $properties */ - public function __construct(PHPClass $class, ?PHPClass $parent, array $properties) + public function __construct(string $name, PHPClass $class, ?PHPClass $parent, array $properties) { $this->class = $class; $this->parent = $parent; @@ -57,6 +63,18 @@ final class RepresentationMetadata implements \JsonSerializable $this->properties[$property->getName()] = $property; } ksort($this->properties); + + $this->name = $name; + } + + /** + * Get name. + * + * @return string + */ + public function getName(): string + { + return $this->name; } /** diff --git a/src/Metadata/DefaultClassHierarchy.php b/src/Metadata/ResourceClassHierarchy.php similarity index 80% rename from src/Metadata/DefaultClassHierarchy.php rename to src/Metadata/ResourceClassHierarchy.php index c15e883f1a2f2945b97d30991cf6524625ac0486..e4bed6fefb279486fbc41fcefff1a251d6e0dae2 100644 --- a/src/Metadata/DefaultClassHierarchy.php +++ b/src/Metadata/ResourceClassHierarchy.php @@ -19,12 +19,13 @@ namespace Irstea\NgModelGeneratorBundle\Metadata; +use ApiPlatform\Core\Metadata\Resource\ResourceNameCollection; use Irstea\NgModelGeneratorBundle\Models\PHPClass; /** - * Class DefaultClassHierarchy. + * Class ResourceClassHierarchy. */ -final class DefaultClassHierarchy implements ClassHierarchy +final class ResourceClassHierarchy implements ClassHierarchy { /** @var PHPClass[] */ private $parents = []; @@ -33,9 +34,19 @@ final class DefaultClassHierarchy implements ClassHierarchy private $children = []; /** - * @param PHPClass[] $classes + * ResourceClassHierarchy constructor. */ - public function preload(PHPClass $class): void + public function __construct(ResourceNameCollection $nameCollection) + { + foreach ($nameCollection->getIterator() as $className) { + $this->preload(PHPClass::get($className)); + } + } + + /** + * @param PHPClass $class + */ + private function preload(PHPClass $class): void { $className = $class->getFullName(); if (\array_key_exists($className, $this->parents)) { diff --git a/src/Metadata/ResourceMetadata.php b/src/Metadata/ResourceMetadata.php index 95eb4b67a366edd4c92a507652cda1f500a0ea7e..43e030b51b38db659cc41997e0a202f3f695ad0f 100644 --- a/src/Metadata/ResourceMetadata.php +++ b/src/Metadata/ResourceMetadata.php @@ -38,26 +38,18 @@ class ResourceMetadata implements \JsonSerializable /** @var bool */ private $abstract; - /** @var SerializationMetadata */ - private $defaultNormalization; - - /** @var SerializationMetadata */ - private $defaultDenormalization; - /** @var OperationMetadata[] */ private $operations = []; /** * ResourceMetadata constructor. * - * @param PHPClass $class - * @param PHPClass|null $parentClass - * @param string $shortName - * @param string $description - * @param bool $abstract - * @param SerializationMetadata $defaultNormalization - * @param SerializationMetadata $defaultDenormalization - * @param OperationMetadata[] $operations + * @param PHPClass $class + * @param PHPClass|null $parentClass + * @param string $shortName + * @param string $description + * @param bool $abstract + * @param OperationMetadata[] $operations */ public function __construct( PHPClass $class, @@ -65,16 +57,12 @@ class ResourceMetadata implements \JsonSerializable string $shortName, string $description, bool $abstract, - SerializationMetadata $defaultNormalization, - SerializationMetadata $defaultDenormalization, array $operations ) { $this->class = $class; $this->parentClass = $parentClass; $this->abstract = $abstract; $this->description = $description; - $this->defaultNormalization = $defaultNormalization; - $this->defaultDenormalization = $defaultDenormalization; foreach ($operations as $operation) { $this->operations[$operation->getName() . $operation->getType()] = $operation->withResource($this); @@ -152,26 +140,6 @@ class ResourceMetadata implements \JsonSerializable return $this->operations; } - /** - * Get defaultNormalization. - * - * @return SerializationMetadata - */ - public function getDefaultNormalization(): SerializationMetadata - { - return $this->defaultNormalization; - } - - /** - * Get defaultDenormalization. - * - * @return SerializationMetadata - */ - public function getDefaultDenormalization(): SerializationMetadata - { - return $this->defaultDenormalization; - } - /** * {@inheritdoc} */ diff --git a/src/ModelGenerator.php b/src/ModelGenerator.php index 00cce57644ecb907dae1032a940572aa103d41cc..c6a9fcaa88244e19d5c7c7dc2f60e14b98e4a4b6 100644 --- a/src/ModelGenerator.php +++ b/src/ModelGenerator.php @@ -24,11 +24,8 @@ use Irstea\NgModelGeneratorBundle\Exceptions\DomainException; use Irstea\NgModelGeneratorBundle\Exceptions\InvalidArgumentException; use Irstea\NgModelGeneratorBundle\Iterators\RecursorIterator; use Irstea\NgModelGeneratorBundle\Iterators\UniqueFilter; -use Irstea\NgModelGeneratorBundle\Metadata\ClassHierarchy; -use Irstea\NgModelGeneratorBundle\Metadata\DefaultClassHierarchy; use Irstea\NgModelGeneratorBundle\Metadata\MetadataFactoryInterface; use Irstea\NgModelGeneratorBundle\Metadata\ResourceMetadata; -use Irstea\NgModelGeneratorBundle\Metadata\SerializationMetadata; use Irstea\NgModelGeneratorBundle\Models\Declaration; use Irstea\NgModelGeneratorBundle\Models\PHPClass; use Irstea\NgModelGeneratorBundle\Models\Types\Alias; @@ -36,8 +33,7 @@ use Irstea\NgModelGeneratorBundle\Models\Types\BuiltinType; use Irstea\NgModelGeneratorBundle\Models\Types\Objects\InterfaceType; use Irstea\NgModelGeneratorBundle\Models\Types\Objects\Property; use Irstea\NgModelGeneratorBundle\Models\Types\Objects\Repository; -use Irstea\NgModelGeneratorBundle\Models\Types\Placeholder; -use Irstea\NgModelGeneratorBundle\Models\Types\Resources\ConcreteRepresentation; +use Irstea\NgModelGeneratorBundle\Models\Types\Resources\UUID; use Irstea\NgModelGeneratorBundle\Models\Types\Type; use Symfony\Component\PropertyInfo\Type as PHPType; use Twig\Environment; @@ -59,15 +55,6 @@ final class ModelGenerator /** @var TypeFactory */ private $typeFactory; - /** @var SerializationMetadata[][] */ - private $defaultSerializations; - - /** @var ConcreteRepresentation[][] */ - private $resources; - - /** @var ClassHierarchy */ - private $classHierarchy; - /** * Serializer constructor. * @@ -102,9 +89,7 @@ final class ModelGenerator } finally { unset( $this->documentation, - $this->typeFactory, - $this->defaultGroups, - $this->resources + $this->typeFactory ); } } @@ -116,12 +101,8 @@ final class ModelGenerator */ private function doGenerate(): string { - $this->classHierarchy = $this->createClassHierachy(); - $this->typeFactory = $this->createTypeFactory(); - $this->defaultSerializations = $this->extractDefaultSerializations(); - [$repositories, $iriPatterns] = $this->extractRepositories(); $declarations = $this->extractDeclarations($repositories); @@ -139,20 +120,6 @@ final class ModelGenerator ); } - /** - * @return ClassHierarchy - */ - private function createClassHierachy(): ClassHierarchy - { - $hierarchy = new DefaultClassHierarchy(); - - foreach ($this->getResourceMetadata() as $class => $meta) { - $hierarchy->preload($class); - } - - return $hierarchy; - } - /** * Crée une usine à types contenant un certain nombre de types par défaut. * @@ -173,9 +140,7 @@ final class ModelGenerator } ); - $factory->getOrCreate('UUID', [Placeholder::class, 'get'], 'UUID'); - $factory->getOrCreate('IRI', [Placeholder::class, 'get'], 'IRI'); - $factory->getOrCreate('Collection', [Placeholder::class, 'get'], 'Collection'); + $factory->getOrCreate('UUID', [UUID::class, 'get']); $factory->getOrCreate( 'CommonFilters', @@ -221,34 +186,6 @@ final class ModelGenerator return new InterfaceType($name, null, $properties); } - /** - * Extrait les groupes de sérialization par défaut des ressources. - * - * @return SerializationMetadata[][] - */ - private function extractDefaultSerializations(): array - { - $serializations = [ - 'normalization' => [], - 'denormalization' => [], - ]; - - /** - * @var PHPClass - * @var ResourceMetadata $resourceMeta - */ - foreach ($this->getResourceMetadata() as $class => $resourceMeta) { - $className = $class->getFullName(); - - $serializations['normalization'][$className] = - $resourceMeta->getDefaultNormalization(); - $serializations['denormalization'][$className] = - $resourceMeta->getDefaultDenormalization(); - } - - return $serializations; - } - /** * Retourne un iterateur sur les métadonnées des ressources. * @@ -278,6 +215,9 @@ final class ModelGenerator * @var ResourceMetadata $resourceMeta */ foreach ($this->getResourceMetadata() as $class => $resourceMeta) { +// printf("\n\n===================================\n\n%s\n", \json_encode($resourceMeta, \JSON_PRETTY_PRINT)); +// continue; + $repoName = $resourceMeta->getShortName() . 'Repository'; $repositories[$repoName] = $this->typeFactory->getOrCreate( diff --git a/src/Models/PHPClass.php b/src/Models/PHPClass.php index a20e719ed27354fbbca2905d31e6668f7dd8c3f6..40a3d96b2fad3d995e4f7107816402fc7af889bb 100644 --- a/src/Models/PHPClass.php +++ b/src/Models/PHPClass.php @@ -90,6 +90,16 @@ final class PHPClass implements \JsonSerializable return new \ReflectionClass($this->getFullName()); } + /** + * @param string|PHPClass $class + * + * @return bool + */ + public function is($class): bool + { + return $this === self::get($class); + } + /** * @return string */ diff --git a/src/Models/Types/Objects/AbstractHierarchicalObject.php b/src/Models/Types/Objects/AbstractHierarchicalObject.php index f64688a0f73d00a04f4326025c52f1bbb9a01554..89d7d4304cbb50edb131482e28c5344bb75b12da 100644 --- a/src/Models/Types/Objects/AbstractHierarchicalObject.php +++ b/src/Models/Types/Objects/AbstractHierarchicalObject.php @@ -34,8 +34,10 @@ abstract class AbstractHierarchicalObject extends AnonymousObject implements Dec /** @var Type|null */ protected $parent; - /** @var Type[] */ - protected $children; + /** + * @var array + */ + private $children; /** * ObjectClass constructor. @@ -44,7 +46,6 @@ abstract class AbstractHierarchicalObject extends AnonymousObject implements Dec * @param Type|null $parent * @param Property[] $properties * @param string $description - * @param Type[] $children */ public function __construct(string $name, ?Type $parent, array $properties = [], string $description = '', array $children = []) { @@ -121,6 +122,7 @@ abstract class AbstractHierarchicalObject extends AnonymousObject implements Dec if ($this->parent) { yield $this->parent; } + yield from $this->children; yield from parent::getIterator(); } diff --git a/src/Models/Types/Resources/AbstractRepresentation.php b/src/Models/Types/Resources/AbstractRepresentation.php deleted file mode 100644 index 44d30a194d9667136b428abb29dd98c9a580caba..0000000000000000000000000000000000000000 --- a/src/Models/Types/Resources/AbstractRepresentation.php +++ /dev/null @@ -1,131 +0,0 @@ -<?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\Models\Types\Resources; - -use Irstea\NgModelGeneratorBundle\Metadata\ResourceMetadata; -use Irstea\NgModelGeneratorBundle\Models\DeclarationTrait; -use Irstea\NgModelGeneratorBundle\Models\Types\AbstractType; -use Irstea\NgModelGeneratorBundle\Models\Types\Type; - -/** - * Class AbstractRepresentation. - */ -final class AbstractRepresentation extends AbstractType implements Representation -{ - use DeclarationTrait; - - /** @var Type|null */ - private $parent; - - /** @var Type[] */ - private $children; - - /** - * AbstractRepresentation constructor. - * - * @param ResourceMetadata $resource - * @param string $name - * @param string $description - * @param Type|null $parent - * @param Type[] $children - */ - public function __construct(string $name, string $description, Type $parent = null, array $children = []) - { - $this->name = $name; - $this->parent = $parent; - $this->children = $children; - $this->description = $description; - } - - /** - * {@inheritdoc} - */ - public function getIterator() - { - if ($this->parent) { - yield $this->parent; - } - yield from $this->children; - } - - /** - * {@inheritdoc} - */ - public function getUsage(): string - { - switch (\count($this->children)) { - case 0: - return 'never'; - case 1: - return $this->children[0]->getUsage(); - default: - return $this->getName(); - } - } - - /** - * {@inheritdoc} - */ - public function getDeclaration(): string - { - if (\count($this->children) < 2) { - return ''; - } - - return \sprintf( - 'export type %s = %s;', - $this->name, - \implode( - ' | ', - \array_map( - function (Type $o) { - return $o->getUsage(); - }, - $this->children - ) - ) - ); - } - - /** - * {@inheritdoc} - */ - public function castToStringOrStringArray(string $expr): string - { - } - - /** - * {@inheritdoc} - */ - public function checkType(string $expr): string - { - } - - /** - * {@inheritdoc} - */ - public function jsonSerialize() - { - return [ - 'parent' => $this->parent, - 'children' => $this->children, - ]; - } -} diff --git a/src/Models/Types/Resources/AtType.php b/src/Models/Types/Resources/AtType.php new file mode 100644 index 0000000000000000000000000000000000000000..be97c7eca556885f979002ff8f9b7daca917724f --- /dev/null +++ b/src/Models/Types/Resources/AtType.php @@ -0,0 +1,90 @@ +<?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\Models\Types\Resources; + +use Irstea\NgModelGeneratorBundle\Models\Types\AbstractType; +use Irstea\NgModelGeneratorBundle\Models\Types\Type; +use Irstea\NgModelGeneratorBundle\TypescriptHelper; + +/** + * Class AtType. + */ +final class AtType extends AbstractType +{ + /** @var Type */ + private $types; + + /** + * IRI constructor. + * + * @param Type[] $types + */ + public function __construct(array $types) + { + $this->types = $types; + } + + /** + * @return array + */ + private function getNames(): array + { + return array_map( + function (Type $t): string { + return $t->getUsage(); + }, + $this->types + ); + } + + /** + * {@inheritdoc} + */ + public function getUsage(): string + { + $names = $this->getNames(); + + return implode(' | ', array_map([TypescriptHelper::class, 'quoteString'], $names)); + } + + /** + * {@inheritdoc} + */ + public function castToStringOrStringArray(string $expr): string + { + return $expr; + } + + /** + * {@inheritdoc} + */ + public function checkType(string $expr): string + { + return sprintf('(%s in [%s])', $expr, $this->getUsage()); + } + + /** + * {@inheritdoc} + */ + public function getIterator() + { + yield from $this->types; + } +} diff --git a/src/Models/Types/Resources/ConcreteRepresentation.php b/src/Models/Types/Resources/ConcreteRepresentation.php deleted file mode 100644 index 952d3c5ab13cdb2eda58823130ccac3a5b4daeee..0000000000000000000000000000000000000000 --- a/src/Models/Types/Resources/ConcreteRepresentation.php +++ /dev/null @@ -1,101 +0,0 @@ -<?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\Models\Types\Resources; - -use Irstea\NgModelGeneratorBundle\Models\PHPClass; -use Irstea\NgModelGeneratorBundle\Models\Types\Objects\InterfaceType; -use Irstea\NgModelGeneratorBundle\Models\Types\Objects\Property; -use Irstea\NgModelGeneratorBundle\Models\Types\Type; -use Irstea\NgModelGeneratorBundle\TypescriptHelper; - -/** - * Class ConcreteRepresentation. - */ -final class ConcreteRepresentation extends InterfaceType implements Representation -{ - /** @var bool */ - private $normalization; - - /** - * @var array|PHPClass[] - */ - private $resources; - - /** - * Representation constructor. - * - * @param string $name - * @param Type|null $parent - * @param Property[] $properties - * @param string $description - * @param PHPClass[] $resources - * @param bool $normalization - */ - public function __construct(string $name, ?Type $parent, array $properties, string $description, array $resources, bool $normalization) - { - parent::__construct($name, $parent, $properties, $description, $resources); - $this->normalization = $normalization; - $this->resources = $resources; - } - - /** - * {@inheritdoc} - */ - protected function getPropertyDeclarations(): array - { - $decl = parent::getPropertyDeclarations(); - - $names = $this->getAllResourceNames(); - $optional = $this->normalization ? '' : '?'; - array_unshift( - $decl, - sprintf("readonly '@id'%s: IRI<%s>;", $optional, implode(' | ', $names)), - sprintf("readonly '@type'%s: %s;", $optional, implode(' | ', array_map([TypescriptHelper::class, 'quoteString'], $names))) - ); - - return $decl; - } - - /** - * {@inheritdoc} - */ - public function checkType(string $expr): string - { - $names = $this->getAllResourceNames(); - if (\count($names) === 1) { - $test = sprintf("%s['@type'] === %s", $expr, TypescriptHelper::quoteString($names[0])); - } else { - $test = sprintf('in [%s]', implode(', ', array_map([TypescriptHelper::class, 'quoteString'], $names))); - } - - return sprintf("(typeof %s === 'object' && '@type' in %s && %s)", $expr, $expr, $test); - } - - /** - * @return string[] - */ - private function getAllResourceNames(): array - { - return array_map( - function (PHPClass $c) { return $c->getBaseName(); }, - $this->resources - ); - } -} diff --git a/src/Models/Types/Resources/IRI.php b/src/Models/Types/Resources/IRI.php index e251a5f937b6104dbf034414a926ec01aa2a2c62..9725e87bbd7f673342920b8834e663457f682f9e 100644 --- a/src/Models/Types/Resources/IRI.php +++ b/src/Models/Types/Resources/IRI.php @@ -19,29 +19,25 @@ namespace Irstea\NgModelGeneratorBundle\Models\Types\Resources; -use Irstea\NgModelGeneratorBundle\Models\MultitonTrait; -use Irstea\NgModelGeneratorBundle\Models\PHPClass; use Irstea\NgModelGeneratorBundle\Models\Types\AbstractType; -use Irstea\NgModelGeneratorBundle\TypescriptHelper; +use Irstea\NgModelGeneratorBundle\Models\Types\Type; /** * Class IRI. */ final class IRI extends AbstractType { - use MultitonTrait; - - /** @var PHPClass */ - private $resource; + /** @var Type */ + private $type; /** * IRI constructor. * - * @param PHPClass $resource + * @param Type $type */ - public function __construct(string $className) + private function __construct(Type $type) { - $this->resource = PHPClass::get($className); + $this->type = $type; } /** @@ -49,7 +45,7 @@ final class IRI extends AbstractType */ public function getUsage(): string { - return sprintf('IRI<%s>', $this->resource->getBaseName()); + return sprintf('IRI<%s>', $this->type->getUsage()); } /** @@ -65,11 +61,42 @@ final class IRI extends AbstractType */ public function checkType(string $expr): string { - return sprintf( - 'isIRI<%s>(%s, %s)', - $this->resource->getBaseName(), - $expr, - TypescriptHelper::quoteString($this->resource->getBaseName()) - ); +// $name = $this->type->getUsage(); +// +// return sprintf( +// 'isIRI<%s>(%s, %s)', +// $name, +// $expr, +// $name +// ); + } + + /** + * {@inheritdoc} + */ + public function getIterator() + { + yield $this->type; + } + + /** + * @param Type $type + * + * @return IRI + */ + public static function get(Type $type): self + { + static $instances = []; + + if ($type instanceof self) { + return $type; + } + + $key = $type->getUsage(); + if (!isset($instances[$key])) { + $instances[$key] = new self($type); + } + + return $instances[$key]; } } diff --git a/src/Models/Types/Resources/Representation.php b/src/Models/Types/Resources/UUID.php similarity index 52% rename from src/Models/Types/Resources/Representation.php rename to src/Models/Types/Resources/UUID.php index 375af4d58b293670ce5ed25d7d2d57370f93cae0..29559eaa3ca64fa4e05baad4784b8359f6f0656f 100644 --- a/src/Models/Types/Resources/Representation.php +++ b/src/Models/Types/Resources/UUID.php @@ -19,12 +19,54 @@ namespace Irstea\NgModelGeneratorBundle\Models\Types\Resources; -use Irstea\NgModelGeneratorBundle\Models\Declaration; -use Irstea\NgModelGeneratorBundle\Models\Types\Type; +use Irstea\NgModelGeneratorBundle\Models\Types\AbstractType; /** - * Interface Representation. + * Class UUID. */ -interface Representation extends Type, Declaration +final class UUID extends AbstractType { + /** + * UUID constructor. + */ + private function __construct() + { + } + + /** + * {@inheritdoc} + */ + public function getUsage(): string + { + return 'UUID'; + } + + /** + * {@inheritdoc} + */ + public function castToStringOrStringArray(string $expr): string + { + return $expr; + } + + /** + * {@inheritdoc} + */ + public function checkType(string $expr): string + { + return parent::checkType($expr); // TODO: Change the autogenerated stub + } + + /** + * @return UUID + */ + public static function get(): self + { + static $instance; + if (!$instance) { + $instance = new self(); + } + + return $instance; + } } diff --git a/src/Models/Types/Union.php b/src/Models/Types/Union.php index f27b8ee1c8472824f7eed3522073ae1a03d9f8b7..8a8198fe58039784a2182b2eacb2476b9bee3c51 100644 --- a/src/Models/Types/Union.php +++ b/src/Models/Types/Union.php @@ -110,7 +110,7 @@ final class Union extends AbstractType case 0: return BuiltinType::get('never'); case 1: - return $types[0]; + return array_shift($types); default: return new Union($types); } diff --git a/src/OperationMapper.php b/src/OperationMapper.php index 90639941f4c44d51c4d54538014c03c6d1e56fde..f1ab13958ae24f85703f2a6e33d15bf4383e63c0 100644 --- a/src/OperationMapper.php +++ b/src/OperationMapper.php @@ -20,6 +20,7 @@ namespace Irstea\NgModelGeneratorBundle; use Doctrine\Common\Inflector\Inflector; +use Irstea\NgModelGeneratorBundle\Metadata\ClassHierarchy; use Irstea\NgModelGeneratorBundle\Metadata\OperationMetadata; use Irstea\NgModelGeneratorBundle\Metadata\SerializationMetadata; use Irstea\NgModelGeneratorBundle\Models\Types\ArrayType; @@ -43,19 +44,6 @@ use Irstea\NgModelGeneratorBundle\Models\Types\Union; */ final class OperationMapper { - /** @var string[][] */ - private const CACHE_DECORATORS = [ - 'item' => [ - 'GET' => ['get(iri, () => %s)', 'iri'], - 'PUT' => ['put(iri, %s)', 'iri'], - 'DELETE' => ['delete(iri, %s)', 'iri'], - 'POST' => ['post(%s)', null], - ], - 'collection' => [ - 'GET' => ['getAll(%s)', null], - ], - ]; - /** @var TypeFactoryInterface */ private $typeFactory; @@ -66,6 +54,7 @@ final class OperationMapper * OperationMapper constructor. * * @param TypeFactoryInterface $typeFactory + * @param ClassHierarchy $classHierarchy * @param OperationMetadata $operation */ public function __construct(TypeFactoryInterface $typeFactory, OperationMetadata $operation) @@ -79,34 +68,28 @@ final class OperationMapper */ public function __invoke(): Operation { - $httpMethod = $this->operation->getMethod(); - $normalization = $this->operation->getNormalization(); - $denormalization = $this->operation->getDenormalization(); - - $isCollection = $this->operation->isCollectionOperation(); - - if ($this->operation->getName() === 'post' && $httpMethod === 'POST' && $normalization && $denormalization) { - $isCollection = false; - } - $requestBody = $responseBody = null; + $normalization = $this->operation->getNormalization(); if ($normalization) { $responseBody = $this->mapSerialization($normalization); - if ($isCollection) { + if ($this->operation->isCollectionOperation()) { $responseBody = new Collection($responseBody); } } + $denormalization = $this->operation->getDenormalization(); if ($denormalization) { $responseBody = $this->mapSerialization($denormalization); - if ($isCollection) { + if ($this->operation->isCollectionOperation()) { $requestBody = new ArrayType($requestBody); } } + $httpMethod = $this->operation->getMethod(); + $clientCall = $this->applyCache( new DirectClientCall( $httpMethod, @@ -114,16 +97,15 @@ final class OperationMapper $this->getFilters(), $responseBody, $requestBody - ), - $isCollection + ) ); return new Operation( - $this->getFancyName($isCollection), + $this->operation->getName(), $clientCall, sprintf( "Operation: %s\nType: %s\nMethod: %s\nPath: %s", - $this->operation->getName(), + $this->operation->getOpDef()->getOriginalName(), $this->operation->getType(), $httpMethod, $this->operation->getPath() @@ -138,7 +120,11 @@ final class OperationMapper */ private function mapSerialization(SerializationMetadata $serializationMetadata): Type { - $mapper = new SerializationMapper($this->typeFactory, $serializationMetadata); + $mapper = new SerializationMapper( + $this->typeFactory, + $serializationMetadata, + !$this->operation->getOpDef()->isCreateItem() + ); return $mapper->get($serializationMetadata->getRoot()->getFullName()); } @@ -198,57 +184,32 @@ final class OperationMapper ); } - /** - * @param bool $isCollection - * - * @return string - */ - private function getFancyName(bool $isCollection): string - { - $name = Inflector::camelize($this->operation->getName()); - - if ($this->operation->getMethod() === 'GET' && strpos($name, 'get') === false) { - $name = 'get' . ucfirst($name); - } - - if ($isCollection) { - if (\in_array($name, ['get', 'put', 'delete', 'patch'], true)) { - return $name . 'All'; - } - - return Inflector::pluralize($name); - } - - return Inflector::singularize($name); - } - /** * @param ClientCall $clientCall * * @return ClientCall */ - private function applyCache(ClientCall $clientCall, bool $isCollection): ClientCall + private function applyCache(ClientCall $clientCall): ClientCall { - $decorators = self::CACHE_DECORATORS[$isCollection ? 'collection' : 'item']; - $method = $this->operation->getMethod(); + $opDef = $this->operation->getOpDef(); - if (!isset($decorators[$method])) { - return $clientCall; + if ($opDef->isGetCollection()) { + return new CachedClientCall($clientCall, 'this.cache.getAll(%s)'); } - - $normalization = $this->operation->getNormalization(); - if (!$normalization) { - if ($method !== 'DELETE') { - return $clientCall; - } + if ($opDef->isCreateItem()) { + return new CachedClientCall($clientCall, 'this.cache.post(%s)'); } - - [$template, $requiredParam] = $decorators[$method]; - if ($requiredParam !== null && !$clientCall->hasParameter($requiredParam)) { - return $clientCall; + if ($opDef->isGetItem()) { + return new CachedClientCall($clientCall, 'this.cache.get(iri, () => %s)'); + } + if ($opDef->isUpdateItem()) { + return new CachedClientCall($clientCall, 'this.cache.put(iri, %s)'); + } + if ($opDef->isDeleteItem()) { + return new CachedClientCall($clientCall, 'this.cache.delete(iri, %s)'); } - return new CachedClientCall($clientCall, 'this.cache.' . $template); + return $clientCall; } /** diff --git a/src/Resources/config/config.xml b/src/Resources/config/config.xml index f693d0aae3c671f0ca74e8380c5e8e7dad80b96a..0552d76b28160f4a669bc09ef3d7cf3a7fe585ae 100644 --- a/src/Resources/config/config.xml +++ b/src/Resources/config/config.xml @@ -17,6 +17,7 @@ <argument type="service" id="api_platform.operation_path_resolver"/> <argument type="service" id="api_platform.filter_locator"/> <argument type="service" id="ng_model_generator.metadata.pagination"/> + <argument type="service" id="irstea_ng_model_generator.metadata.resource_class_hierarchy"/> </service> <service id="ng_model_generator.metadata.caching_factory" lazy="true" public="false" @@ -26,6 +27,11 @@ <argument type="service" id="doctrine_cache.providers.ng_model_generator_metadata_cache" on-invalid="null"/> </service> + <service id="irstea_ng_model_generator.metadata.resource_class_hierarchy" + class="Irstea\NgModelGeneratorBundle\Metadata\ResourceClassHierarchy" > + <argument id="irstea_ng_model_generator.resource_name_collection" type="service"/> + </service> + <service id="ng_model_generator.metadata.pagination" lazy="true" class="Irstea\NgModelGeneratorBundle\Metadata\PaginationMetadata"> <argument>%api_platform.collection.pagination.enabled%</argument> diff --git a/src/SerializationMapper.php b/src/SerializationMapper.php index 8aaaf48d9242c150f7ca5e0fd2c640c77146a0e4..0e8c5a9536c247c458899fdf4bd6d51a591f5a2d 100644 --- a/src/SerializationMapper.php +++ b/src/SerializationMapper.php @@ -21,15 +21,21 @@ namespace Irstea\NgModelGeneratorBundle; use Irstea\NgModelGeneratorBundle\Exceptions\DomainException; use Irstea\NgModelGeneratorBundle\Metadata\PropertyMetadata; +use Irstea\NgModelGeneratorBundle\Metadata\RepresentationMetadata; use Irstea\NgModelGeneratorBundle\Metadata\SerializationMetadata; use Irstea\NgModelGeneratorBundle\Models\PHPClass; +use Irstea\NgModelGeneratorBundle\Models\Types\Alias; use Irstea\NgModelGeneratorBundle\Models\Types\ArrayType; use Irstea\NgModelGeneratorBundle\Models\Types\BuiltinType; +use Irstea\NgModelGeneratorBundle\Models\Types\Objects\AnonymousObject; +use Irstea\NgModelGeneratorBundle\Models\Types\Objects\InterfaceType; use Irstea\NgModelGeneratorBundle\Models\Types\Objects\Property; -use Irstea\NgModelGeneratorBundle\Models\Types\Resources\ConcreteRepresentation; +use Irstea\NgModelGeneratorBundle\Models\Types\Placeholder; +use Irstea\NgModelGeneratorBundle\Models\Types\Resources\AtType; use Irstea\NgModelGeneratorBundle\Models\Types\Resources\IRI; use Irstea\NgModelGeneratorBundle\Models\Types\Type; use Irstea\NgModelGeneratorBundle\Models\Types\Union; +use Symfony\Component\PropertyInfo\Type as APIType; /** * Class SerializationMapper. @@ -42,24 +48,27 @@ final class SerializationMapper implements TypeFactoryInterface /** @var SerializationMetadata */ private $serialization; - /** @var int[] */ - private $refCounts; + /** @var array */ + private $classInfo; - /** @var PHPClass[] */ - private $children; + /** @var bool */ + private $withAtFields; /** * SerializationMapper constructor. * * @param TypeFactoryInterface $typeFactory * @param SerializationMetadata $serialization + * @param bool $withAtFields */ public function __construct( TypeFactoryInterface $typeFactory, - SerializationMetadata $serialization + SerializationMetadata $serialization, + bool $withAtFields ) { $this->typeFactory = $typeFactory; $this->serialization = $serialization; + $this->withAtFields = $withAtFields; } /** @@ -87,13 +96,20 @@ final class SerializationMapper implements TypeFactoryInterface return $this->typeFactory->get($name); } - $class = PHPClass::get($name); - if ($this->serialization->hasRepresentationOf($class)) { - $reprName = $this->getRepresentationName($class); + if ($this->classInfo === null) { + $this->init(); + } + + if (isset($this->classInfo[$name])) { + $class = PHPClass::get($name); + $repr = $this->serialization->getRepresentationOf($class); - return $this->typeFactory->getOrCreate($reprName, function () use ($class, $reprName) { - return $this->mapRepresentation($class, $reprName); - }); + return $this->typeFactory->getOrCreate( + $repr->getName(), + function () use ($repr) { + return $this->mapRepresentation($repr); + } + ); } return $this->getOrCreate($name, [$this, 'get'], 'any'); @@ -116,74 +132,89 @@ final class SerializationMapper implements TypeFactoryInterface } /** - * @param PHPClass $class - * - * @return string - */ - private function getRepresentationName(PHPClass $class): string - { - $prefix = ($this->serialization->isNormalization() ? 'Get' : 'Put') . $this->serialization->getRoot()->getBaseName(); - - return (implode('', $this->serialization->getGroups()) ?: $prefix) . $class->getBaseName(); - } - - /** - * @param string $name - * @param PHPClass $resourceClass + * @param RepresentationMetadata $repr * * @return Type */ - private function mapRepresentation(PHPClass $resourceClass, string $reprName): Type + private function mapRepresentation(RepresentationMetadata $repr): Type { + $resourceClass = $repr->getClass(); + /** - * @var PHPClass + * @var bool + * @var PHPClass $parentClass * @var PropertyMetadata[] $propertiesMeta + * @var PHPClass[] $descendantClasses */ - [, $parentClass, $propertiesMeta] = $this->doMapRepresentation($resourceClass); - - $properties = $this->mapProperties($propertiesMeta); - $parent = $parentClass ? $this->get($parentClass->getFullName()) : null; + [$keep, $parentClass, $propertiesMeta, $descendantClasses] = $this->classInfo[$resourceClass->getFullName()]; -// $repr = $this->serialization->getRepresentationOf($resourceClass); -// -// $parent = $repr->getParent() ? $this->get($repr->getParent()->getFullName()) : null; -// $properties = $this->mapProperties($repr->getProperties()); + if (!$keep) { + if ($propertiesMeta) { + return new AnonymousObject($this->mapProperties($propertiesMeta)); + } + if ($descendantClasses) { + $descendants = array_map( + function (PHPClass $c) { + return $this->get($c->getFullName()); + }, + $descendantClasses + ); + + return new Alias($repr->getName(), Union::create($descendants)); + } - $desc = []; - $desc[] = 'Resource: ' . $resourceClass->getFullName(); -// $desc[] = 'Direction: ' . ($normalization ? 'response' : 'request'); -// $desc[] = sprintf('Serialization groups: %s', $groups ? implode(', ', $groups) : '-'); - $desc = trim(implode("\n", $desc)); + return IRI::get(Placeholder::get($resourceClass->getBaseName())); + } - return new ConcreteRepresentation($reprName, $parent, $properties, $desc, $this->getCoveredResources($resourceClass), $this->serialization->isNormalization()); - } + $parent = $parentClass ? $this->get($parentClass->getFullName()) : null; - /** - * @param PHPClass $class - * - * @return array - */ - private function doMapRepresentation(PHPClass $class): array - { - $repr = $this->serialization->getRepresentationOf($class); - $properties = $repr->getProperties(); + $properties = $this->mapProperties($propertiesMeta); - $parentClass = $repr->getParent(); - if ($parentClass) { - [$keep, , $parentProperties] = $this->doMapRepresentation($parentClass); - if (!$keep) { - $parentClass = null; - $properties = array_replace($parentProperties, $properties); + $children = []; + /** @var PHPClass $descendant */ + foreach ($descendantClasses as $descendant) { + if (!$descendant->is($resourceClass)) { + $children[] = $this->get($descendant->getFullName()); } } - if (!$this->serialization->isRoot($class)) { - if (!$parentClass && $this->getRefCount($class) < 2) { - return [false, null, $properties]; + if ($children && !$parent && !$properties) { + return new Alias($repr->getName(), Union::create($children)); + } + + if ($this->withAtFields) { + $names = []; + foreach ($descendantClasses as $descendant) { + $resName = $descendant->getBaseName(); + $names[$resName] = Placeholder::get($resName); } + + $properties['@id'] = new Property( + '@id', + '', + IRI::get(Union::create($names)), + true, + !$this->serialization->isNormalization(), + true + ); + + $properties['@type'] = new Property( + '@type', + '', + new AtType($names), + true, + !$this->serialization->isNormalization(), + true + ); } - return [true, $parentClass, $properties]; + $desc = []; + $desc[] = 'Resource: ' . $resourceClass->getFullName(); + $desc[] = 'Direction: ' . ($this->serialization->isNormalization() ? 'response' : 'request'); + $desc[] = sprintf('Serialization groups: %s', implode(', ', $this->serialization->getGroups()) ?: '-'); + $desc = trim(implode("\n", $desc)); + + return new InterfaceType($repr->getName(), $parent, $properties, $desc, $children); } /** @@ -197,10 +228,6 @@ final class SerializationMapper implements TypeFactoryInterface $identifierCount = 0; foreach ($propertiesMeta as $propertyMeta) { - if (!$this->acceptProperty($propertyMeta)) { - continue; - } - if ($propertyMeta->isIdentifier()) { ++$identifierCount; } @@ -216,23 +243,6 @@ final class SerializationMapper implements TypeFactoryInterface return $properties; } - /** - * @param PropertyMetadata $propertyMeta - * - * @return bool - */ - public function acceptProperty(PropertyMetadata $propertyMeta): bool - { - if (!$propertyMeta->getType()) { - return false; - } - if ($this->serialization->isNormalization()) { - return $propertyMeta->isReadable(); - } - - return $propertyMeta->isWritable() || $propertyMeta->isInitializable(); - } - /** * @param PropertyMetadata $propertyMeta * @@ -251,25 +261,19 @@ final class SerializationMapper implements TypeFactoryInterface } if ($propertyMeta->isLink()) { - $linkedClassName = $leafType->getClassName(); - $type = IRI::get($linkedClassName); - - if ($propertyMeta->isEmbedded()) { - $linkedType = $this->get($linkedClassName); - if ($this->serialization->isNormalization()) { - $type = $linkedType; - } else { - $type = Union::create([$type, $linkedType]); - } - } + $type = $this->get($leafType->getClassName()); } else { $type = $this->get($leafType->getClassName() ?: $leafType->getBuiltinType()); } $number = BuiltinType::get('number'); - foreach (\array_reverse($collections) as $indexType) { + foreach (\array_reverse($collections) as $indexTypeMeta) { + /** @var APIType $indexTypeMeta */ + $indexType = $this->get($indexTypeMeta->getClassName() ?: $indexTypeMeta->getBuiltinType()); if ($indexType === $number) { $type = new ArrayType($type); + } else { + throw new DomainException("Cannot create collection with key $indexType"); } } @@ -283,77 +287,116 @@ final class SerializationMapper implements TypeFactoryInterface ); } - /** - * @param PHPClass|string $class - * - * @return int - */ - private function getRefCount($class): int - { - if (!$this->refCounts) { - $this->initRefCounts(); - } - - return $this->refCounts[PHPClass::get($class)->getFullName()] ?? 0; - } - - private function initRefCounts(): void + private function init(): void { $reprs = $this->serialization->getRepresentations(); - $this->refCounts = \array_fill_keys(array_keys($reprs), 0); + $refCounts = \array_fill_keys(array_keys($reprs), 0); + $children = \array_fill_keys(array_keys($reprs), []); foreach ($reprs as $repr) { - if ($repr->getParent()) { - ++$this->refCounts[$repr->getParent()->getFullName()]; + $parent = $repr->getParent(); + if ($parent) { + ++$refCounts[$parent->getFullName()]; + $children[$parent->getFullName()][] = $repr->getClass(); } + foreach ($repr->getProperties() as $property) { $className = $property->getLeafType()->getClassName(); if ($className && isset($this->refCounts[$className])) { - ++$this->refCounts[$className]; + ++$refCounts[$className]; } } } + + foreach ($reprs as $repr) { + $this->initInfo($repr, $refCounts); + } + + foreach ($this->classInfo as $className => &$ci) { + $ci[3] = $this->initConcreteDescendants(PHPClass::get($className), $children); + } + unset($ci); + +// if ($this->serialization->getRoot()->getBaseName() === 'Location') { +// printf("%s\n", json_encode($this->classInfo, \JSON_PRETTY_PRINT)); +// die; +// } } /** - * @param PHPClass|string $class + * @param RepresentationMetadata $repr + * @param array $refCount * - * @return int + * @return array */ - private function getCoveredResources($class): array + private function initInfo(RepresentationMetadata $repr, array $refCount): array { - $resources = [$class]; - - for ($i = 0; $i < \count($resources); ++$i) { - $resources = array_merge($resources, $this->getChildren($resources[$i])); + $className = $repr->getClass()->getFullName(); + if (!isset($this->classInfo[$className])) { + $this->classInfo[$className] = $this->doInitInfo($repr, $refCount); } - return $resources; + return $this->classInfo[$className]; } /** - * @param PHPClass|string $class + * @param RepresentationMetadata $repr + * @param array $refCount * - * @return int + * @return array */ - private function getChildren($class): array + private function doInitInfo(RepresentationMetadata $repr, array $refCount): array { - if (!$this->children) { - $this->initChildren(); + $class = $repr->getClass(); + $className = $class->getFullName(); + $properties = $repr->getProperties(); + + $parentClass = $repr->getParent(); + if ($parentClass) { + [$keep, , $parentProperties, ] = $this->initInfo( + $this->serialization->getRepresentationOf($parentClass), + $refCount + ); + if (!$keep) { + $parentClass = null; + $properties = array_replace($parentProperties, $properties); + } } - return $this->children[PHPClass::get($class)->getFullName()] ?? []; + if (!$this->serialization->isRoot($class)) { + if (!$parentClass && $refCount[$className] < 2) { + return [false, null, $properties, []]; + } + if (!$properties) { + return [false, null, [], []]; + } + } + + return [true, $parentClass, $properties, []]; } - private function initChildren(): void + /** + * @param PHPClass $class + * @param PHPClass[][] $children + * + * @return PHPClass[] + */ + private function initConcreteDescendants(PHPClass $class, array $children): array { - $reprs = $this->serialization->getRepresentations(); - $this->children = \array_fill_keys(array_keys($reprs), []); - - foreach ($reprs as $repr) { - if ($repr->getParent()) { - $this->children[$repr->getParent()->getFullName()][] = $repr->getClass(); + /** @var PHPClass[] $queue */ + $queue = [$class]; + /** @var PHPClass[] $descendantsqueue */ + $descendants = []; + + while ($queue) { + $current = array_shift($queue); + if ($this->classInfo[$current->getFullName()][0]) { + $descendants[] = $current; } + /** @noinspection SlowArrayOperationsInLoopInspection */ + $queue = array_merge($queue, $children[$current->getFullName()]); } + + return $descendants; } }