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