From 63c249b74f20f719f997eabcaea19a5dc5a9f3e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guillaume=20Perr=C3=A9al?= <guillaume.perreal@irstea.fr> Date: Thu, 27 Sep 2018 14:29:09 +0200 Subject: [PATCH] =?UTF-8?q?D=C3=A9l=C3=A9gation=20de=20la=20g=C3=A9n=C3=A9?= =?UTF-8?q?ration=20des=20m=C3=A9tadonn=C3=A9es=20de=20propri=C3=A9t=C3=A9?= =?UTF-8?q?s=20=C3=A0=20une=20classe=20s=C3=A9par=C3=A9e.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Metadata/MetadataFactory.php | 188 ++++++------------ src/Metadata/PropertyMetadata.php | 18 +- src/Metadata/PropertyMetadataFactory.php | 235 +++++++++++++++++++++++ 3 files changed, 311 insertions(+), 130 deletions(-) create mode 100644 src/Metadata/PropertyMetadataFactory.php diff --git a/src/Metadata/MetadataFactory.php b/src/Metadata/MetadataFactory.php index 529edfa..086c03f 100644 --- a/src/Metadata/MetadataFactory.php +++ b/src/Metadata/MetadataFactory.php @@ -25,7 +25,6 @@ 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; @@ -71,11 +70,12 @@ final class MetadataFactory implements MetadataFactoryInterface /** @var SerializationMetadata[] */ private $serializations = []; - /** - * @var ClassHierarchy - */ + /** @var ClassHierarchy */ private $classHierarchy; + /** @var string[][] */ + private $defaultGroups = []; + /** * MetadataFactory constructor. * @@ -349,28 +349,21 @@ final class MetadataFactory implements MetadataFactoryInterface private function doGetSerialization(ClassName $class, bool $normalization, ?OperationDef $opDef, array $groups): SerializationMetadata { if ($normalization) { - $metadata = $this->resourceMetadataFactory->create($class->getFullName()); - $defaultGroups = $metadata->getAttribute('normalization_context', [])['groups'] ?? []; - sort($defaultGroups); - if ($defaultGroups === $groups) { - $selfNamePrefix = ''; - } else { - $selfNamePrefix = implode('', $groups); - } - } else { - $selfNamePrefix = Inflector::classify($opDef ? $opDef->getOriginalName() : implode('', $groups)); - } - $otherNamePrefix = $selfNamePrefix . $class->getBaseName(); - - if ($normalization) { - $propFilter = 'filterGetProperty'; + $mode = PropertyMetadataFactory::MODE_READ; } elseif ($opDef && $opDef->isCreateItem()) { - $propFilter = 'filterCreateProperty'; + $mode = PropertyMetadataFactory::MODE_CREATE; } elseif ($opDef && $opDef->isUpdateItem()) { - $propFilter = 'filterUpdateProperty'; + $mode = PropertyMetadataFactory::MODE_UPDATE; } else { - $propFilter = 'filterAnyProperty'; + $mode = PropertyMetadataFactory::MODE_OTHER; } + $propertyMetadataFactory = new PropertyMetadataFactory( + $this->propertyNameCollectionFactory, + $this->propertyMetadataFactory, + $class, + $mode, + $groups + ); /** @var RepresentationMetadata[] $reprs */ $representations = []; @@ -394,22 +387,21 @@ final class MetadataFactory implements MetadataFactoryInterface $queue[] = $children; } - $propertiesMeta = $this->getPropertiesMeta($current, $groups); + $propertiesMeta = $propertyMetadataFactory->getAPIMetadata($current); $properties = []; foreach ($propertiesMeta as $propertyName => $propertyMeta) { - if (!$this->$propFilter($class, $propertyName, $propertyMeta)) { - continue; - } - $property = $this->mapProperty($current, $propertyName, $propertyMeta); + $property = $propertyMetadataFactory->create($current, $propertyName); $properties[$propertyName] = $property; - $type = $property->getLeafType(); - if ($type->getClassName()) { - $queue[] = PHPClass::get($type->getClassName()); + if ($property->isEmbedded()) { + $type = $property->getLeafType(); + if ($type->getClassName()) { + $queue[] = PHPClass::get($type->getClassName()); + } } } - $name = ($current === $class ? $selfNamePrefix : $otherNamePrefix) . $current->getBaseName(); + $name = $this->getRepresentationName($class, $current, $normalization, $opDef, $groups); $abstract = (new \ReflectionClass($current->getFullName()))->isAbstract(); @@ -420,121 +412,59 @@ final class MetadataFactory implements MetadataFactoryInterface } /** - * @param ClassName $class - * @param string $name - * @param APIPropertyMetadata $property + * @param ClassName $root + * @param ClassName $class + * @param bool $normalization + * @param OperationDef|null $opDef + * @param array $groups * - * @return bool + * @throws \ApiPlatform\Core\Exception\ResourceClassNotFoundException * - * @internal + * @return string */ - public function filterGetProperty(ClassName $class, string $name, APIPropertyMetadata $property): bool + private function getRepresentationName(ClassName $root, ClassName $class, bool $normalization, ?OperationDef $opDef, array $groups): string { - return $property->isIdentifier() || $property->isReadable() || $property->isReadableLink(); - } + if ($normalization && (!$groups || $groups === $this->getDefaultGroups($class))) { + return $class->getBaseName(); + } - /** - * @param ClassName $class - * @param string $name - * @param APIPropertyMetadata $property - * - * @return bool - * - * @internal - */ - public function filterCreateProperty(ClassName $class, string $name, APIPropertyMetadata $property): bool - { - return $property->isWritable() || $property->isWritableLink() || ($property->isInitializable() ?: false); - } + if ($opDef) { + $name = $opDef->getOriginalName(); + } elseif ($groups) { + $name = implode('', $groups); + } else { + $name = $normalization ? 'Read' : 'Write'; + } - /** - * @param ClassName $class - * @param string $name - * @param APIPropertyMetadata $property - * - * @return bool - * - * @internal - */ - public function filterUpdateProperty(ClassName $class, string $name, APIPropertyMetadata $property): bool - { - return $this->filterGetProperty($class, $name, $property) || $property->isWritable() || $property->isWritableLink(); - } + if (strpos($name, $root->getBaseName()) === false) { + $name .= $root->getBaseName(); + } - /** - * @param ClassName $class - * @param string $name - * @param APIPropertyMetadata $property - * - * @return bool - * - * @internal - */ - public function filterAnyProperty(ClassName $class, string $name, APIPropertyMetadata $property): bool - { - return true; + if (strpos($name, $class->getBaseName()) === false) { + $name .= $class->getBaseName(); + } + + return Inflector::classify($name); } /** * @param ClassName $class - * @param array $groups - * - * @throws \ApiPlatform\Core\Exception\PropertyNotFoundException - * @throws \ApiPlatform\Core\Exception\ResourceClassNotFoundException * - * @return APIPropertyMetadata[] + * @return array */ - private function getPropertiesMeta(ClassName $class, array $groups): array + private function getDefaultGroups(ClassName $class): 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; + $className = $class->getFullName(); + if (isset($this->defaultGroups[$className])) { + return $this->defaultGroups[$className]; } - return $properties; - } + $context = $this->resourceMetadataFactory->create($className)->getAttribute('normalization_context', []); + $groups = $context['groups'] ?? []; - /** - * @param ClassName $class - * @param string $propertyName - * @param APIPropertyMetadata $propertyMeta - * - * @return PropertyMetadata - */ - private function mapProperty(ClassName $class, string $propertyName, APIPropertyMetadata $propertyMeta): PropertyMetadata - { - $leafType = $typeMeta = $propertyMeta->getType(); + sort($groups); + $this->defaultGroups[$className] = $groups; - 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 - ); + return $groups; } } diff --git a/src/Metadata/PropertyMetadata.php b/src/Metadata/PropertyMetadata.php index a14515f..e86def0 100644 --- a/src/Metadata/PropertyMetadata.php +++ b/src/Metadata/PropertyMetadata.php @@ -51,6 +51,9 @@ class PropertyMetadata implements \JsonSerializable, HasName /** @var bool */ private $link; + /** @var bool */ + private $embedded; + /** * PropertyMetadata constructor. * @@ -62,6 +65,7 @@ class PropertyMetadata implements \JsonSerializable, HasName * @param bool $writable * @param bool $initializable * @param bool $link + * @param bool $embedded */ public function __construct( string $name, @@ -71,7 +75,8 @@ class PropertyMetadata implements \JsonSerializable, HasName bool $readable, bool $writable, bool $initializable, - bool $link + bool $link, + bool $embedded ) { $this->name = $name; $this->description = $description; @@ -81,6 +86,7 @@ class PropertyMetadata implements \JsonSerializable, HasName $this->writable = $writable; $this->initializable = $initializable; $this->link = $link; + $this->embedded = $embedded; } /** @@ -184,6 +190,16 @@ class PropertyMetadata implements \JsonSerializable, HasName return $this->link; } + /** + * Get embedded. + * + * @return bool + */ + public function isEmbedded(): bool + { + return $this->embedded; + } + /** * {@inheritdoc} */ diff --git a/src/Metadata/PropertyMetadataFactory.php b/src/Metadata/PropertyMetadataFactory.php new file mode 100644 index 0000000..1c2c1bb --- /dev/null +++ b/src/Metadata/PropertyMetadataFactory.php @@ -0,0 +1,235 @@ +<?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\Exception\ResourceClassNotFoundException; +use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface; +use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; +use ApiPlatform\Core\Metadata\Property\PropertyMetadata as APIPropertyMetadata; +use Irstea\NgModelGeneratorBundle\Models\ClassName; +use Irstea\NgModelGeneratorBundle\Models\PHPClass; +use Symfony\Component\PropertyInfo\Type; + +/** + * Class PropertyMetadataFactory. + */ +class PropertyMetadataFactory +{ + public const MODE_CREATE = 'CREATE'; + public const MODE_UPDATE = 'UPDATE'; + public const MODE_READ = 'READ'; + public const MODE_OTHER = 'OTHER'; + + /** + * Mode des objets relatifs en fonction du mode de la resource. + */ + private const RELATED_MODES = [ + self::MODE_CREATE => self::MODE_UPDATE, + self::MODE_UPDATE => self::MODE_UPDATE, + self::MODE_READ => self::MODE_READ, + self::MODE_OTHER => self::MODE_OTHER, + ]; + + /** @var PropertyNameCollectionFactoryInterface */ + private $propertyNameCollectionFactory; + + /** @var PropertyMetadataFactoryInterface */ + private $propertyMetadataFactory; + + /** @var ClassName */ + private $resource; + + /** @var bool */ + private $normalization; + + /** @var string */ + private $mode; + + /** @var array */ + private $groups; + + /** @var array */ + private $properties = []; + + /** + * PropertyMetadataFactory constructor. + * + * @param PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory + * @param PropertyMetadataFactoryInterface $propertyMetadataFactory + * @param ClassName $resource + * @param string $mode + * @param array $groups + */ + public function __construct( + PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, + PropertyMetadataFactoryInterface $propertyMetadataFactory, + ClassName $resource, + string $mode, + array $groups + ) { + $this->propertyNameCollectionFactory = $propertyNameCollectionFactory; + $this->propertyMetadataFactory = $propertyMetadataFactory; + $this->resource = $resource; + $this->mode = $mode; + $this->groups = $groups; + } + + /** + * @param ClassName $class + * @param string $propertyName + * @param APIPropertyMetadata $propertyMeta + * + * @return PropertyMetadata + */ + public function create(ClassName $class, string $propertyName): PropertyMetadata + { + $propertyMeta = $this->getAPIMetadata($class)[$propertyName]; + + $typeMeta = $propertyMeta->getType(); + \assert($typeMeta !== null); + + $mode = $this->getMode($class); + $link = $embedded = $mode === self::MODE_READ ? $propertyMeta->isReadableLink() : $propertyMeta->isWritableLink(); + + if (!$embedded) { + $leafType = $this->getLeafType($typeMeta); + $leafClass = $leafType ? $leafType->getClassName() : null; + if ($leafClass) { + try { + $leafProperties = $this->getAPIMetadata(PHPClass::get($leafClass)); + $embedded = \count($leafProperties) > 0; + $link = true; + } catch (ResourceClassNotFoundException $ex) { + // NOOP + } + } + } + + return new PropertyMetadata( + $propertyName, + '', + $typeMeta, + $propertyMeta->isIdentifier(), + $propertyMeta->isReadable(), + $propertyMeta->isWritable(), + $propertyMeta->isInitializable() ?: false, + $link ?: false, + $embedded ?: false + ); + } + + /** + * @param Type $type + * + * @return Type|null + */ + private function getLeafType(Type $type): ?Type + { + while ($type && $type->isCollection()) { + $type = $type->getCollectionValueType(); + } + + return $type; + } + + /** + * @param ClassName $class + * + * @return string + */ + private function getMode(ClassName $class): string + { + $mode = $this->mode; + if ($class->getFullName() === $this->resource->getFullName()) { + $mode = self::RELATED_MODES[$mode]; + } + + return $mode; + } + + /** + * @param ClassName $class + * + * @throws ResourceClassNotFoundException + * @throws \ApiPlatform\Core\Exception\PropertyNotFoundException + * + * @return APIPropertyMetadata[] + */ + public function getAPIMetadata(ClassName $class): array + { + $key = $class->getFullName(); + if (!isset($this->properties[$key])) { + $this->properties[$key] = $this->doGetAPIMetadata($class); + } + + return $this->properties[$key]; + } + + /** + * @param ClassName $class + * + * @throws \ApiPlatform\Core\Exception\PropertyNotFoundException + * @throws ResourceClassNotFoundException + * + * @return APIPropertyMetadata[] + */ + private function doGetAPIMetadata(ClassName $class): array + { + $mode = $this->getMode($class); + $properties = []; + $options = $this->groups ? ['serializer_groups' => $this->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; + } + + if (!$this->acceptProperty($mode, $propertyMeta)) { + continue; + } + + $properties[$propertyName] = $propertyMeta; + } + + return $properties; + } + + /** + * @param string $mode + * @param APIPropertyMetadata $propertyMeta + * + * @return bool + */ + private function acceptProperty(string $mode, APIPropertyMetadata $propertyMeta): bool + { + switch ($mode) { + case self::MODE_CREATE: + return $propertyMeta->isWritable() || $propertyMeta->isInitializable(); + case self::MODE_READ: + return $propertyMeta->isReadable(); + default: + return $propertyMeta->isReadable() || $propertyMeta->isWritable(); + } + } +} -- GitLab