Commit 63c249b7 authored by Guillaume Perréal's avatar Guillaume Perréal
Browse files

Délégation de la génération des métadonnées de propriétés à une classe séparée.

Showing with 311 additions and 130 deletions
+311 -130
...@@ -25,7 +25,6 @@ use ApiPlatform\Core\Api\OperationType; ...@@ -25,7 +25,6 @@ use ApiPlatform\Core\Api\OperationType;
use ApiPlatform\Core\Api\ResourceClassResolverInterface; use ApiPlatform\Core\Api\ResourceClassResolverInterface;
use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface; use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; 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\Factory\ResourceMetadataFactoryInterface;
use ApiPlatform\Core\Metadata\Resource\ResourceMetadata as APIResourceMetadata; use ApiPlatform\Core\Metadata\Resource\ResourceMetadata as APIResourceMetadata;
use ApiPlatform\Core\PathResolver\OperationPathResolverInterface; use ApiPlatform\Core\PathResolver\OperationPathResolverInterface;
...@@ -71,11 +70,12 @@ final class MetadataFactory implements MetadataFactoryInterface ...@@ -71,11 +70,12 @@ final class MetadataFactory implements MetadataFactoryInterface
/** @var SerializationMetadata[] */ /** @var SerializationMetadata[] */
private $serializations = []; private $serializations = [];
/** /** @var ClassHierarchy */
* @var ClassHierarchy
*/
private $classHierarchy; private $classHierarchy;
/** @var string[][] */
private $defaultGroups = [];
/** /**
* MetadataFactory constructor. * MetadataFactory constructor.
* *
...@@ -349,28 +349,21 @@ final class MetadataFactory implements MetadataFactoryInterface ...@@ -349,28 +349,21 @@ final class MetadataFactory implements MetadataFactoryInterface
private function doGetSerialization(ClassName $class, bool $normalization, ?OperationDef $opDef, array $groups): SerializationMetadata private function doGetSerialization(ClassName $class, bool $normalization, ?OperationDef $opDef, array $groups): SerializationMetadata
{ {
if ($normalization) { if ($normalization) {
$metadata = $this->resourceMetadataFactory->create($class->getFullName()); $mode = PropertyMetadataFactory::MODE_READ;
$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';
} elseif ($opDef && $opDef->isCreateItem()) { } elseif ($opDef && $opDef->isCreateItem()) {
$propFilter = 'filterCreateProperty'; $mode = PropertyMetadataFactory::MODE_CREATE;
} elseif ($opDef && $opDef->isUpdateItem()) { } elseif ($opDef && $opDef->isUpdateItem()) {
$propFilter = 'filterUpdateProperty'; $mode = PropertyMetadataFactory::MODE_UPDATE;
} else { } else {
$propFilter = 'filterAnyProperty'; $mode = PropertyMetadataFactory::MODE_OTHER;
} }
$propertyMetadataFactory = new PropertyMetadataFactory(
$this->propertyNameCollectionFactory,
$this->propertyMetadataFactory,
$class,
$mode,
$groups
);
/** @var RepresentationMetadata[] $reprs */ /** @var RepresentationMetadata[] $reprs */
$representations = []; $representations = [];
...@@ -394,22 +387,21 @@ final class MetadataFactory implements MetadataFactoryInterface ...@@ -394,22 +387,21 @@ final class MetadataFactory implements MetadataFactoryInterface
$queue[] = $children; $queue[] = $children;
} }
$propertiesMeta = $this->getPropertiesMeta($current, $groups); $propertiesMeta = $propertyMetadataFactory->getAPIMetadata($current);
$properties = []; $properties = [];
foreach ($propertiesMeta as $propertyName => $propertyMeta) { foreach ($propertiesMeta as $propertyName => $propertyMeta) {
if (!$this->$propFilter($class, $propertyName, $propertyMeta)) { $property = $propertyMetadataFactory->create($current, $propertyName);
continue;
}
$property = $this->mapProperty($current, $propertyName, $propertyMeta);
$properties[$propertyName] = $property; $properties[$propertyName] = $property;
$type = $property->getLeafType(); if ($property->isEmbedded()) {
if ($type->getClassName()) { $type = $property->getLeafType();
$queue[] = PHPClass::get($type->getClassName()); 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(); $abstract = (new \ReflectionClass($current->getFullName()))->isAbstract();
...@@ -420,121 +412,59 @@ final class MetadataFactory implements MetadataFactoryInterface ...@@ -420,121 +412,59 @@ final class MetadataFactory implements MetadataFactoryInterface
} }
/** /**
* @param ClassName $class * @param ClassName $root
* @param string $name * @param ClassName $class
* @param APIPropertyMetadata $property * @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();
}
/** if ($opDef) {
* @param ClassName $class $name = $opDef->getOriginalName();
* @param string $name } elseif ($groups) {
* @param APIPropertyMetadata $property $name = implode('', $groups);
* } else {
* @return bool $name = $normalization ? 'Read' : 'Write';
* }
* @internal
*/
public function filterCreateProperty(ClassName $class, string $name, APIPropertyMetadata $property): bool
{
return $property->isWritable() || $property->isWritableLink() || ($property->isInitializable() ?: false);
}
/** if (strpos($name, $root->getBaseName()) === false) {
* @param ClassName $class $name .= $root->getBaseName();
* @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, $class->getBaseName()) === false) {
* @param ClassName $class $name .= $class->getBaseName();
* @param string $name }
* @param APIPropertyMetadata $property
* return Inflector::classify($name);
* @return bool
*
* @internal
*/
public function filterAnyProperty(ClassName $class, string $name, APIPropertyMetadata $property): bool
{
return true;
} }
/** /**
* @param ClassName $class * @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 = []; $className = $class->getFullName();
$options = $groups ? ['serializer_groups' => $groups] : []; if (isset($this->defaultGroups[$className])) {
return $this->defaultGroups[$className];
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; $context = $this->resourceMetadataFactory->create($className)->getAttribute('normalization_context', []);
} $groups = $context['groups'] ?? [];
/** sort($groups);
* @param ClassName $class $this->defaultGroups[$className] = $groups;
* @param string $propertyName
* @param APIPropertyMetadata $propertyMeta
*
* @return PropertyMetadata
*/
private function mapProperty(ClassName $class, string $propertyName, APIPropertyMetadata $propertyMeta): PropertyMetadata
{
$leafType = $typeMeta = $propertyMeta->getType();
while ($leafType && $leafType->isCollection()) { return $groups;
$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
);
} }
} }
...@@ -51,6 +51,9 @@ class PropertyMetadata implements \JsonSerializable, HasName ...@@ -51,6 +51,9 @@ class PropertyMetadata implements \JsonSerializable, HasName
/** @var bool */ /** @var bool */
private $link; private $link;
/** @var bool */
private $embedded;
/** /**
* PropertyMetadata constructor. * PropertyMetadata constructor.
* *
...@@ -62,6 +65,7 @@ class PropertyMetadata implements \JsonSerializable, HasName ...@@ -62,6 +65,7 @@ class PropertyMetadata implements \JsonSerializable, HasName
* @param bool $writable * @param bool $writable
* @param bool $initializable * @param bool $initializable
* @param bool $link * @param bool $link
* @param bool $embedded
*/ */
public function __construct( public function __construct(
string $name, string $name,
...@@ -71,7 +75,8 @@ class PropertyMetadata implements \JsonSerializable, HasName ...@@ -71,7 +75,8 @@ class PropertyMetadata implements \JsonSerializable, HasName
bool $readable, bool $readable,
bool $writable, bool $writable,
bool $initializable, bool $initializable,
bool $link bool $link,
bool $embedded
) { ) {
$this->name = $name; $this->name = $name;
$this->description = $description; $this->description = $description;
...@@ -81,6 +86,7 @@ class PropertyMetadata implements \JsonSerializable, HasName ...@@ -81,6 +86,7 @@ class PropertyMetadata implements \JsonSerializable, HasName
$this->writable = $writable; $this->writable = $writable;
$this->initializable = $initializable; $this->initializable = $initializable;
$this->link = $link; $this->link = $link;
$this->embedded = $embedded;
} }
/** /**
...@@ -184,6 +190,16 @@ class PropertyMetadata implements \JsonSerializable, HasName ...@@ -184,6 +190,16 @@ class PropertyMetadata implements \JsonSerializable, HasName
return $this->link; return $this->link;
} }
/**
* Get embedded.
*
* @return bool
*/
public function isEmbedded(): bool
{
return $this->embedded;
}
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
......
<?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();
}
}
}
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment