فهرست منبع

refactor SchemaValidationService

Olivier Massot 1 سال پیش
والد
کامیت
29f961cc8a

+ 3 - 1
src/Commands/Doctrine/SchemaValidateCommand.php

@@ -6,6 +6,7 @@ namespace App\Commands\Doctrine;
 
 use App\Service\Doctrine\SchemaValidation\Difference;
 use App\Service\Doctrine\SchemaValidation\DiffTypeEnum;
+use App\Service\Doctrine\SchemaValidation\SchemaSnippetsMaker;
 use App\Service\Doctrine\SchemaValidation\SchemaValidationService;
 use Symfony\Component\Console\Attribute\AsCommand;
 use Symfony\Component\Console\Command\Command;
@@ -24,6 +25,7 @@ class SchemaValidateCommand extends Command
 {
     public function __construct(
         private readonly SchemaValidationService $schemaValidationService,
+        private readonly SchemaSnippetsMaker $schemaSnippetsMaker
     ) {
         parent::__construct();
     }
@@ -78,7 +80,7 @@ class SchemaValidateCommand extends Command
         }
 
         if ($input->getOption('snippets')) {
-            $this->schemaValidationService->makeSnippets($diff);
+            $this->schemaSnippetsMaker->makeSnippets($diff);
             $output->writeln("Snippets generated");
         }
 

+ 458 - 0
src/Service/Doctrine/SchemaValidation/SchemaSnippetsMaker.php

@@ -0,0 +1,458 @@
+<?php
+declare(strict_types=1);
+
+namespace App\Service\Doctrine\SchemaValidation;
+
+use ApiPlatform\Metadata\ApiResource;
+use App\Service\Utils\EntityUtils;
+use App\Service\Utils\FileUtils;
+use App\Service\Utils\Path;
+use Doctrine\Common\Collections\Collection;
+use Doctrine\ORM\Mapping\ClassMetadataInfo;
+use Doctrine\ORM\Mapping\Column;
+use Doctrine\ORM\Mapping\Entity;
+use Doctrine\ORM\Mapping\JoinColumn;
+use Doctrine\ORM\Mapping\ManyToMany;
+use Doctrine\ORM\Mapping\ManyToOne;
+use Doctrine\ORM\Mapping\OneToMany;
+use Doctrine\ORM\Mapping\OneToOne;
+use Nette\PhpGenerator\Attribute;
+use Nette\PhpGenerator\ClassType;
+use Nette\PhpGenerator\Method;
+use Nette\PhpGenerator\Parameter;
+use Nette\PhpGenerator\PhpFile;
+use Nette\PhpGenerator\PhpNamespace;
+use Nette\PhpGenerator\Property;
+use Nette\PhpGenerator\PsrPrinter;
+use RuntimeException;
+
+/**
+ * Service produisant les snippets des entités, propriétés et méthodes
+ * manquantes dans les entités de la V2.
+ *
+ * Les résultats sont enregistrés dans le répertoire `~/schema_validation_snippets`.
+ *
+ * @see https://github.com/nette/php-generator
+ */
+class SchemaSnippetsMaker
+{
+    public function __construct(
+        private readonly EntityUtils $entityUtils
+    )
+    {}
+
+    /**
+     * Make entities snippets from a 'diff' array,
+     * as generated by SchemaValidationService::validateSchema()
+     *
+     * @param array<string, Difference | array<Difference>> $diff
+     * @return void
+     */
+    public function makeSnippets(array $diff): void
+    {
+        $snippetsDir = Path::join(Path::getProjectDir(), 'schema_validation_snippets');
+
+        if (is_dir($snippetsDir)) {
+            FileUtils::rrmDir($snippetsDir);
+        }
+        mkdir($snippetsDir, 0777, true);
+
+        $printer = new PsrPrinter;
+
+        foreach ($diff as $entity => $differences) {
+            if (empty($differences)) {
+                continue;
+            }
+
+            $class = $this->makeSnippetEntityClass($entity);
+
+            $methods = [];
+
+            if (is_array($differences)) {
+                foreach ($differences as $field => $difference) {
+                    if (!is_array($difference->getExpectedType())
+                    ) {
+                        $prop = $this->makeSnippetEntitySimpleProp($field, $difference->getExpectedType());
+                        $methods[] = $this->makeSnippetGetterForProp($prop);
+                        $methods[] = $this->makeSnippetSetterForProp($prop);
+
+                    } else {
+                        $prop = $this->makeSnippetEntityCollectionProp($field, $difference->getExpectedType());
+                        $methods[] = $this->makeSnippetGetterForProp($prop);
+
+                        if (
+                            isset($difference->getExpectedType()['type']) &&
+                            in_array($difference->getExpectedType()['type'], [ClassMetadataInfo::ONE_TO_MANY, ClassMetadataInfo::MANY_TO_MANY])
+                        ) {
+                            $methods[] = $this->makeSnippetAdderForCollection($prop);
+                            $methods[] = $this->makeSnippetRemoverForCollection($prop);
+                        } else {
+                            $methods[] = $this->makeSnippetSetterForProp($prop);
+                        }
+                    }
+                    $class->addMember($prop);
+                }
+            } else {
+                $class->addProperty('id', 'int');
+            }
+
+            foreach ($methods as $method) {
+                $class->addMember($method);
+            }
+
+            $file = new PhpFile;
+            $file->addComment('This file is auto-generated.');
+            $file->setStrictTypes();
+
+            $namespaceValue = $this->entityUtils->getNamespaceFromName($entity) ?? ('App\\Entity\\' . $entity);
+
+            $namespace = new PhpNamespace($namespaceValue);
+            $namespace->addUse(ApiResource::class);
+            $namespace->addUse('Doctrine\Common\Collections\ArrayCollection');
+            $namespace->addUse('Doctrine\Common\Collections\Collection');
+            $namespace->addUse('Doctrine\ORM\Mapping', 'ORM');
+
+            if (is_array($differences)) {
+                foreach ($this->getEntityToImportFromRelations($differences) as $use) {
+                    $namespace->addUse($use);
+                }
+            }
+
+            $namespace->add($class);
+
+            $file->addNamespace($namespace);
+
+            try {
+                $fullname = $this->entityUtils->getFullNameFromEntityName($entity);
+            } catch (\LogicException) {
+                $fullname = $entity;
+            }
+
+            if ($fullname === $entity) {
+                var_dump('Entity namespace not found: ' . $entity);
+            }
+
+            $relativePath = str_replace('\\', '/', $fullname) . '.php';
+
+            $fileName = Path::join($snippetsDir, $relativePath);
+
+            if (!is_dir(dirname($fileName))) {
+                mkdir(dirname($fileName), 0777, true);
+            }
+
+            $f = fopen($fileName, 'w+');
+            try {
+                fwrite($f, $printer->printFile($file));
+            } finally {
+                fclose($f);
+            }
+        }
+    }
+
+    /**
+     * Génère un objet ClassType
+     * @see https://api.nette.org/php-generator/master/Nette/PhpGenerator/ClassType.html
+     *
+     * @param string $entity
+     * @return ClassType
+     */
+    protected function makeSnippetEntityClass(string $entity): ClassType
+    {
+        $class = new ClassType($entity);
+
+        $class->setAttributes([
+            new Attribute(ApiResource::class, ['operations' => []]),
+            new Attribute(Entity::class, [])
+        ]);
+
+        return $class;
+    }
+
+    /**
+     * Parcourt les propriétés de type relations OneToOne ou ManyToOne pour lister
+     * les classes d'entités à importer (pour le typage de ces propriétés).
+     *
+     * @param array<string, Difference> $differences
+     * @return array<string>
+     */
+    protected function getEntityToImportFromRelations(array $differences): array
+    {
+        $imports = [];
+
+        foreach ($differences as $field => $difference) {
+            if (
+                !is_array($difference->getExpectedType()) ||
+                !in_array(isset($difference->getExpectedType()['type']), [ClassMetadataInfo::ONE_TO_ONE, ClassMetadataInfo::MANY_TO_ONE]) ||
+                !isset($difference->getExpectedType()['targetEntity'])
+            ) {
+                continue;
+            }
+
+            $entityName = $this->entityUtils->getEntityNameFromFullName($difference->getExpectedType()['targetEntity']);
+            if (!$entityName) {
+                continue;
+            }
+
+            try {
+                $fullName = $this->entityUtils->getFullNameFromEntityName($entityName);
+            } catch (\LogicException) {
+                continue;
+            }
+
+            $imports[] = $fullName;
+        }
+
+        return $imports;
+    }
+
+    /**
+     * Make a Property object for a simple field (not a relation)
+     * @see https://api.nette.org/php-generator/master/Nette/PhpGenerator/Property.html
+     *
+     * @param string $name
+     * @param string $type
+     * @return Property
+     */
+    protected function makeSnippetEntitySimpleProp(string $name, string $type): Property
+    {
+        $php_type = $type;
+        $php_type = str_replace('text', 'string', $php_type);
+        $php_type = str_replace('boolean', 'bool', $php_type);
+        $php_type = str_replace('integer', 'int', $php_type);
+        $php_type = str_replace('datetime', '?\DateTimeInterface', $php_type);
+        $php_type = str_replace('json_array', 'array', $php_type);
+
+        $prop = new Property($name);
+        $prop->setPrivate();
+        $prop->setType($php_type);
+
+        if ($type === 'text') {
+            $prop->addAttribute(Column::class, ['length' => 255, 'options' => ['nullable' => true]]);
+        } elseif ($type === 'integer') {
+            $prop->addAttribute(Column::class, ['type' => 'integer', 'options' => ['nullable' => true]]);
+        } elseif ($type === 'boolean') {
+            $prop->addAttribute(Column::class, ['options' => ['default' => false]]);
+        } elseif ($type === 'datetime') {
+            $prop->addAttribute(Column::class, ['type' => 'date', 'options' => ['nullable' => true]]);
+        } elseif ($type === 'json_array') {
+            $prop->addAttribute(Column::class, ['type' => 'json', 'options' => ['nullable' => true]]);
+        } else {
+            $prop->addAttribute(Column::class, []);
+        }
+
+        return $prop;
+    }
+
+    /**
+     * Make a Property object for a relation field
+     * @see https://api.nette.org/php-generator/master/Nette/PhpGenerator/Property.html
+     *
+     * @param string $name
+     * @param array $type
+     * @return Property
+     */
+    protected function makeSnippetEntityCollectionProp(string $name, array $type): Property
+    {
+        $prop = new Property($name);
+        $prop->setType('Collection');
+
+        if ($type['type'] === ClassMetadataInfo::ONE_TO_MANY) {
+            $prop->setType(Collection::class);
+
+            $options = [];
+            if (isset($type['mappedBy'])) {
+                $options['mappedBy'] = $type['mappedBy'];
+            }
+            if (isset($type['targetEntity'])) {
+                $options['targetEntity'] = $this->entityUtils->getEntityNameFromFullName($type['targetEntity']) . '::class';
+            }
+            if (isset($type['inversedBy'])) {
+                $options['inversedBy'] = $type['inversedBy'];
+            }
+            if (isset($type['cascade'])) {
+                $options['cascade'] = $type['cascade'];
+            } else {
+                $options['cascade'] = ['persist'];
+            }
+            if (isset($type['orphanRemoval'])) {
+                $options['orphanRemoval'] = $type['orphanRemoval'];
+            }
+
+            $prop->addAttribute(OneToMany::class, $options);
+        } else if ($type['type'] === ClassMetadataInfo::MANY_TO_MANY) {
+            $prop->setType(Collection::class);
+
+            $options = [];
+            if (isset($type['mappedBy'])) {
+                $options['mappedBy'] = $type['mappedBy'];
+            }
+            if (isset($type['targetEntity'])) {
+                $options['targetEntity'] = $this->entityUtils->getEntityNameFromFullName($type['targetEntity']) . '::class';
+            }
+            if (isset($type['inversedBy'])) {
+                $options['inversedBy'] = $type['inversedBy'];
+            }
+            if (isset($type['cascade'])) {
+                $options['cascade'] = $type['cascade'];
+            } else {
+                $options['cascade'] = ['persist'];
+            }
+            if (isset($type['orphanRemoval'])) {
+                $options['orphanRemoval'] = $type['orphanRemoval'];
+            }
+
+            $prop->addAttribute(ManyToMany::class, $options);
+        } else if ($type['type'] === ClassMetadataInfo::ONE_TO_ONE) {
+            $newType = 'mixed';
+            if (isset($type['targetEntity'])) {
+                $targetEntityName = $this->entityUtils->getEntityNameFromFullName($type['targetEntity']);
+                try {
+                    $newType = $this->entityUtils->getFullNameFromEntityName($targetEntityName);
+                } catch (\LogicException) {}
+            }
+
+            $prop->setType($newType);
+
+            $options = [];
+            if (isset($type['inversedBy'])) {
+                $options['inversedBy'] = $type['inversedBy'];
+            }
+            if (isset($type['targetEntity'])) {
+                $options['targetEntity'] = $this->entityUtils->getEntityNameFromFullName($type['targetEntity']) . '::class';
+            }
+            if (isset($type['cascade'])) {
+                $options['cascade'] = $type['cascade'];
+            } else {
+                $options['cascade'] = ['persist'];
+            }
+            $prop->addAttribute(OneToOne::class, $options);
+            $prop->addAttribute(JoinColumn::class, []);
+
+        } else if ($type['type'] === ClassMetadataInfo::MANY_TO_ONE) {
+            $newType = 'mixed';
+            if (isset($type['targetEntity'])) {
+                $targetEntityName = $this->entityUtils->getEntityNameFromFullName($type['targetEntity']);
+                try {
+                    $newType = $this->entityUtils->getFullNameFromEntityName($targetEntityName);
+                } catch (\LogicException) {}
+            }
+            $prop->setType($newType);
+
+            $options = [];
+            if (isset($type['cascade'])) {
+                $options['cascade'] = $type['cascade'];
+            }
+            if (isset($type['inversedBy'])) {
+                $options['inversedBy'] = $type['inversedBy'];
+            }
+
+            $prop->addAttribute(ManyToOne::class, $options);
+            $prop->addAttribute(JoinColumn::class, ['referencedColumnName' => 'id', 'nullable' => false, 'onDelete' => 'SET NULL']);
+
+        } else {
+            throw new RuntimeException('Unknown relation type');
+        }
+
+        return $prop;
+    }
+
+    /**
+     * Make a 'getter' method for the given property
+     *
+     * @param Property $prop
+     * @return Method
+     */
+    protected function makeSnippetGetterForProp(Property $prop): Method {
+        $method = new Method('get' . ucfirst($prop->getName()));
+        $method->setReturnType($prop->getType());
+        $method->setBody('return $this->' . $prop->getName() . ';');
+        return $method;
+    }
+
+    /**
+     * Make a 'setter' method for the given property
+     *
+     * @param Property $prop
+     * @return Method
+     */
+    protected function makeSnippetSetterForProp(Property $prop): Method {
+        $method = new Method('set' . ucfirst($prop->getName()));
+
+        $parameter = new Parameter($prop->getName());
+        $parameter->setType($prop->getType());
+        $method->setParameters([$parameter]);
+
+        $method->setReturnType('self');
+        $method->setBody(
+            implode(
+                "\n",
+                [
+                    '$this->' . $prop->getName() . ' = $' . $prop->getName() . ';',
+                    'return $this;',
+                ]
+            )
+        );
+        return $method;
+    }
+
+    /**
+     * Make an 'adder' method for the given property
+     *
+     * @param Property $prop
+     * @return Method
+     */
+    protected function makeSnippetAdderForCollection(Property $prop): Method {
+
+        $singularPropName = rtrim($prop->getName(), 's');
+
+        $method = new Method('add' . ucfirst($singularPropName));
+
+        $parameter = new Parameter($singularPropName);
+        $parameter->setType($prop->getType());
+        $method->setParameters([$parameter]);
+
+        $method->setReturnType('self');
+        $method->setBody(
+            implode(
+                "\n",
+                [
+                    'if (!$this->' . $prop->getName() . '->contains($' . $singularPropName . ')) {',
+                    '    $this->' . $prop->getName() . '[] = $' . $singularPropName . ';',
+                    '}',
+                    '',
+                    'return $this;',
+                ]
+            )
+        );
+        return $method;
+    }
+
+    /**
+     * Make a 'remover' method for the given property
+     *
+     * @param Property $prop
+     * @return Method
+     */
+    protected function makeSnippetRemoverForCollection(Property $prop): Method {
+        $singularPropName = rtrim($prop->getName(), 's');
+
+        $method = new Method('remove' . ucfirst($singularPropName));
+
+        $parameter = new Parameter($singularPropName);
+        $parameter->setType($prop->getType());
+        $method->setParameters([$parameter]);
+
+        $method->setReturnType('self');
+        $method->setBody(
+            implode(
+                "\n",
+                [
+                    '$this->' . $prop->getName() . '->removeElement($' . $singularPropName . ');',
+                    '',
+                    'return $this;',
+                ]
+            )
+        );
+        return $method;
+    }
+}

+ 17 - 418
src/Service/Doctrine/SchemaValidation/SchemaValidationService.php

@@ -3,30 +3,11 @@ declare(strict_types=1);
 
 namespace App\Service\Doctrine\SchemaValidation;
 
-use ApiPlatform\Metadata\ApiResource;
 use App\Service\ApiLegacy\ApiLegacyRequestService;
-use App\Service\Utils\FileUtils;
-use App\Service\Utils\Path;
-use Doctrine\Common\Collections\Collection;
+use App\Service\Utils\EntityUtils;
 use Doctrine\ORM\EntityManagerInterface;
 use Doctrine\ORM\Mapping\ClassMetadataInfo;
-use Doctrine\ORM\Mapping\Column;
-use Doctrine\ORM\Mapping\Entity;
-use Doctrine\ORM\Mapping\JoinColumn;
-use Doctrine\ORM\Mapping\ManyToMany;
-use Doctrine\ORM\Mapping\ManyToOne;
 use Doctrine\ORM\Mapping\MappingException;
-use Doctrine\ORM\Mapping\OneToMany;
-use Doctrine\ORM\Mapping\OneToOne;
-use Nette\PhpGenerator\Attribute;
-use Nette\PhpGenerator\ClassType;
-use Nette\PhpGenerator\Method;
-use Nette\PhpGenerator\Parameter;
-use Nette\PhpGenerator\PhpFile;
-use Nette\PhpGenerator\PhpNamespace;
-use Nette\PhpGenerator\Printer;
-use Nette\PhpGenerator\Property;
-use Nette\PhpGenerator\PsrPrinter;
 use RuntimeException;
 use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
 use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface;
@@ -40,14 +21,12 @@ use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
  */
 class SchemaValidationService
 {
-    protected array $entityNamesMapping = [];
-
     public function __construct(
         private readonly EntityManagerInterface  $entityManager,
         private readonly ApiLegacyRequestService $apiLegacyRequestService,
+        private readonly EntityUtils $entityUtils
     )
-    {
-    }
+    {}
 
     /**
      * Compare the V2 doctrine schema to the one in V1, and return a list of differences,
@@ -82,9 +61,7 @@ class SchemaValidationService
         $schema = [];
 
         foreach ($metadata as $entityMetadata) {
-            $entityClassName = $this->fullNameToEntityName($entityMetadata->getName());
-
-            $this->entityNamesMapping[$entityClassName] = $entityMetadata->getName();
+            $entityClassName = $this->entityUtils->getEntityNameFromFullName($entityMetadata->getName());
 
             $schema[$entityClassName] = [];
 
@@ -264,26 +241,6 @@ class SchemaValidationService
         return isset($schema[$entity][$relation]) && is_array($schema[$entity][$relation]);
     }
 
-    /**
-     * Get the name of a relation from a ClassMetadataInfo integer constant.
-     *
-     * @param array<string, string|int> $relation
-     * @return string
-     */
-    protected function getRelationTypeLabel(array $relation): string
-    {
-        if ($relation['type'] === ClassMetadataInfo::ONE_TO_ONE) {
-            return 'OneToOne';
-        } elseif ($relation['type'] === ClassMetadataInfo::MANY_TO_ONE) {
-            return 'ManyToOne';
-        } elseif ($relation['type'] === ClassMetadataInfo::ONE_TO_MANY) {
-            return 'OneToMany';
-        } elseif ($relation['type'] === ClassMetadataInfo::MANY_TO_MANY) {
-            return 'ManyToMany';
-        }
-        throw new RuntimeException('Unknown relation type');
-    }
-
     /**
      * Look up for differences in $relationCompared compared to $relationReference, and return
      * a Difference if any, or null else.
@@ -303,7 +260,7 @@ class SchemaValidationService
         }
 
         if (
-            $this->fullNameToEntityName($relationReference['targetEntity']) !== $this->fullNameToEntityName($relationCompared['targetEntity'])
+            $this->entityUtils->getEntityNameFromFullName($relationReference['targetEntity']) !== $this->entityUtils->getEntityNameFromFullName($relationCompared['targetEntity'])
         ) {
             return new Difference(
                 DiffTypeEnum::DIFFERENT_RELATION_CONFIGURATION,
@@ -326,380 +283,22 @@ class SchemaValidationService
     }
 
     /**
-     * Extract an entity base name from a fully qualified name
+     * Get the name of a relation from a ClassMetadataInfo integer constant.
      *
-     * @param string $fullName
+     * @param array<string, string|int> $relation
      * @return string
      */
-    protected function fullNameToEntityName(string $fullName): string
-    {
-        $parts = explode('\\', $fullName);
-        return array_pop($parts);
-    }
-
-    protected function getFullNameFromEntityName(string $entityName): ?string
-    {
-        return $this->entityNamesMapping[$entityName] ?? null;
-    }
-
-    protected function getNamespaceFromEntityName(string $entityName): ?string
-    {
-        $fullName = $this->getFullNameFromEntityName($entityName);
-        if (!$fullName) {
-            return null;
-        }
-        $parts = explode('\\', $fullName);
-        array_pop($parts);
-        return implode('\\', $parts);
-    }
-
-    public function makeSnippets(array $diff): void
-    {
-        $snippetsDir = Path::join(Path::getProjectDir(), 'schema_validation_snippets');
-
-        if (is_dir($snippetsDir)) {
-            FileUtils::rrmDir($snippetsDir);
-        }
-        mkdir($snippetsDir, 0777, true);
-
-        $printer = new PsrPrinter;
-
-        foreach ($diff as $entity => $differences) {
-            if (empty($differences)) {
-                continue;
-            }
-
-            $class = $this->makeSnippetEntityClass($entity);
-
-            $methods = [];
-
-            if (is_array($differences)) {
-                foreach ($differences as $field => $difference) {
-                    if (!is_array($difference->getExpectedType())
-                    ) {
-                        $prop = $this->makeSnippetEntitySimpleProp($field, $difference->getExpectedType());
-                        $methods[] = $this->makeSnippetGetterForProp($prop);
-                        $methods[] = $this->makeSnippetSetterForProp($prop);
-
-                    } else {
-                        $prop = $this->makeSnippetEntityCollectionProp($field, $difference->getExpectedType());
-                        $methods[] = $this->makeSnippetGetterForProp($prop);
-
-                        if (
-                            isset($difference->getExpectedType()['type']) &&
-                            in_array($difference->getExpectedType()['type'], [ClassMetadataInfo::ONE_TO_MANY, ClassMetadataInfo::MANY_TO_MANY])
-                        ) {
-                            $methods[] = $this->makeSnippetAdderForCollection($prop);
-                            $methods[] = $this->makeSnippetRemoverForCollection($prop);
-                        } else {
-                            $methods[] = $this->makeSnippetSetterForProp($prop);
-                        }
-                    }
-                    $class->addMember($prop);
-                }
-            } else {
-                $class->addProperty('id', 'int');
-            }
-
-            foreach ($methods as $method) {
-                $class->addMember($method);
-            }
-
-            $file = new PhpFile;
-            $file->addComment('This file is auto-generated.');
-            $file->setStrictTypes();
-
-            $namespaceValue = $this->getNamespaceFromEntityName($entity) ?? ('App\\Entity\\' . $entity);
-
-            $namespace = new PhpNamespace($namespaceValue);
-            $namespace->addUse(ApiResource::class);
-            $namespace->addUse('Doctrine\Common\Collections\ArrayCollection');
-            $namespace->addUse('Doctrine\Common\Collections\Collection');
-            $namespace->addUse('Doctrine\ORM\Mapping', 'ORM');
-
-            if (is_array($differences)) {
-                foreach ($this->getEntityToImportFromRelations($differences) as $use) {
-                    $namespace->addUse($use);
-                }
-            }
-
-            $namespace->add($class);
-
-            $file->addNamespace($namespace);
-
-            $fullname = $this->getFullNameFromEntityName($entity) ?? $entity;
-            if ($fullname === $entity) {
-                var_dump('Entity namespace not found: ' . $entity);
-            }
-
-            $relativePath = str_replace('\\', '/', $fullname) . '.php';
-
-            $fileName = Path::join($snippetsDir, $relativePath);
-
-            if (!is_dir(dirname($fileName))) {
-                mkdir(dirname($fileName), 0777, true);
-            }
-
-            $f = fopen($fileName, 'w+');
-            try {
-                fwrite($f, $printer->printFile($file));
-            } finally {
-                fclose($f);
-            }
-        }
-    }
-
-    protected function makeSnippetEntityClass(string $entity): ClassType
-    {
-        $class = new ClassType($entity);
-
-        $class->setAttributes([
-            new Attribute(ApiResource::class, ['operations' => []]),
-            new Attribute(Entity::class, [])
-        ]);
-
-        return $class;
-    }
-
-    protected function getEntityToImportFromRelations(array $differences): array
-    {
-        $imports = [];
-
-        foreach ($differences as $field => $difference) {
-            if (
-                !is_array($difference->getExpectedType()) ||
-                !in_array(isset($difference->getExpectedType()['type']), [ClassMetadataInfo::ONE_TO_ONE, ClassMetadataInfo::MANY_TO_ONE]) ||
-                !isset($difference->getExpectedType()['targetEntity'])
-            ) {
-                continue;
-            }
-
-            $fullName = $this->fullNameToEntityName($difference->getExpectedType()['targetEntity']);
-            if (!$fullName) {
-                continue;
-            }
-
-            $entityName = $this->getFullNameFromEntityName($fullName);
-            if (!$entityName) {
-                continue;
-            }
-
-            $imports[] = $entityName;
-        }
-
-        return $imports;
-    }
-
-    protected function makeSnippetEntitySimpleProp(string $name, string $type): Property
-    {
-        $php_type = $type;
-        $php_type = str_replace('text', 'string', $php_type);
-        $php_type = str_replace('boolean', 'bool', $php_type);
-        $php_type = str_replace('integer', 'int', $php_type);
-        $php_type = str_replace('datetime', '?\DateTimeInterface', $php_type);
-        $php_type = str_replace('json_array', 'array', $php_type);
-
-        $prop = new Property($name);
-        $prop->setPrivate();
-        $prop->setType($php_type);
-
-        if ($type === 'text') {
-            $prop->addAttribute(Column::class, ['length' => 255, 'options' => ['nullable' => true]]);
-        } elseif ($type === 'integer') {
-            $prop->addAttribute(Column::class, ['type' => 'integer', 'options' => ['nullable' => true]]);
-        } elseif ($type === 'boolean') {
-            $prop->addAttribute(Column::class, ['options' => ['default' => false]]);
-        } elseif ($type === 'datetime') {
-            $prop->addAttribute(Column::class, ['type' => 'date', 'options' => ['nullable' => true]]);
-        } elseif ($type === 'json_array') {
-            $prop->addAttribute(Column::class, ['type' => 'json', 'options' => ['nullable' => true]]);
-        } else {
-            $prop->addAttribute(Column::class, []);
-        }
-
-        return $prop;
-    }
-
-    protected function makeSnippetEntityCollectionProp(string $name, array $type): Property
+    protected function getRelationTypeLabel(array $relation): string
     {
-        $prop = new Property($name);
-        $prop->setType('Collection');
-
-        if ($type['type'] === ClassMetadataInfo::ONE_TO_MANY) {
-            $prop->setType(Collection::class);
-
-            $options = [];
-            if (isset($type['mappedBy'])) {
-                $options['mappedBy'] = $type['mappedBy'];
-            }
-            if (isset($type['targetEntity'])) {
-                $options['targetEntity'] = $this->fullNameToEntityName($type['targetEntity']) . '::class';
-            }
-            if (isset($type['inversedBy'])) {
-                $options['inversedBy'] = $type['inversedBy'];
-            }
-            if (isset($type['cascade'])) {
-                $options['cascade'] = $type['cascade'];
-            } else {
-                $options['cascade'] = ['persist'];
-            }
-            if (isset($type['orphanRemoval'])) {
-                $options['orphanRemoval'] = $type['orphanRemoval'];
-            }
-
-            $prop->addAttribute(OneToMany::class, $options);
-        } else if ($type['type'] === ClassMetadataInfo::MANY_TO_MANY) {
-            $prop->setType(Collection::class);
-
-            $options = [];
-            if (isset($type['mappedBy'])) {
-                $options['mappedBy'] = $type['mappedBy'];
-            }
-            if (isset($type['targetEntity'])) {
-                $options['targetEntity'] = $this->fullNameToEntityName($type['targetEntity']) . '::class';
-            }
-            if (isset($type['inversedBy'])) {
-                $options['inversedBy'] = $type['inversedBy'];
-            }
-            if (isset($type['cascade'])) {
-                $options['cascade'] = $type['cascade'];
-            } else {
-                $options['cascade'] = ['persist'];
-            }
-            if (isset($type['orphanRemoval'])) {
-                $options['orphanRemoval'] = $type['orphanRemoval'];
-            }
-
-            $prop->addAttribute(ManyToMany::class, $options);
-        } else if ($type['type'] === ClassMetadataInfo::ONE_TO_ONE) {
-            $newType = 'mixed';
-            if (isset($type['targetEntity'])) {
-                $targetEntityName = $this->fullNameToEntityName($type['targetEntity']);
-                $localFullName = $this->getFullNameFromEntityName($targetEntityName);
-                if ($localFullName) {
-                    $newType = $localFullName;
-                }
-            }
-
-            $prop->setType($newType);
-
-            $options = [];
-            if (isset($type['inversedBy'])) {
-                $options['inversedBy'] = $type['inversedBy'];
-            }
-            if (isset($type['targetEntity'])) {
-                $options['targetEntity'] = $this->fullNameToEntityName($type['targetEntity']) . '::class';
-            }
-            if (isset($type['cascade'])) {
-                $options['cascade'] = $type['cascade'];
-            } else {
-                $options['cascade'] = ['persist'];
-            }
-            $prop->addAttribute(OneToOne::class, $options);
-            $prop->addAttribute(JoinColumn::class, []);
-
-        } else if ($type['type'] === ClassMetadataInfo::MANY_TO_ONE) {
-            $newType = 'mixed';
-            if (isset($type['targetEntity'])) {
-                $targetEntityName = $this->fullNameToEntityName($type['targetEntity']);
-                $localFullName = $this->getFullNameFromEntityName($targetEntityName);
-                if ($localFullName) {
-                    $newType = $localFullName;
-                }
-            }
-            $prop->setType($newType);
-
-            $options = [];
-            if (isset($type['cascade'])) {
-                $options['cascade'] = $type['cascade'];
-            }
-            if (isset($type['inversedBy'])) {
-                $options['inversedBy'] = $type['inversedBy'];
-            }
-
-            $prop->addAttribute(ManyToOne::class, $options);
-            $prop->addAttribute(JoinColumn::class, ['referencedColumnName' => 'id', 'nullable' => false, 'onDelete' => 'SET NULL']);
-
-        } else {
-            throw new RuntimeException('Unknown relation type');
+        if ($relation['type'] === ClassMetadataInfo::ONE_TO_ONE) {
+            return 'OneToOne';
+        } elseif ($relation['type'] === ClassMetadataInfo::MANY_TO_ONE) {
+            return 'ManyToOne';
+        } elseif ($relation['type'] === ClassMetadataInfo::ONE_TO_MANY) {
+            return 'OneToMany';
+        } elseif ($relation['type'] === ClassMetadataInfo::MANY_TO_MANY) {
+            return 'ManyToMany';
         }
-
-        return $prop;
-    }
-
-    protected function makeSnippetGetterForProp(Property $prop): Method {
-        $method = new Method('get' . ucfirst($prop->getName()));
-        $method->setReturnType($prop->getType());
-        $method->setBody('return $this->' . $prop->getName() . ';');
-        return $method;
-    }
-
-    protected function makeSnippetSetterForProp(Property $prop): Method {
-        $method = new Method('set' . ucfirst($prop->getName()));
-
-        $parameter = new Parameter($prop->getName());
-        $parameter->setType($prop->getType());
-        $method->setParameters([$parameter]);
-
-        $method->setReturnType('self');
-        $method->setBody(
-            implode(
-                "\n",
-                [
-                    '$this->' . $prop->getName() . ' = $' . $prop->getName() . ';',
-                    'return $this;',
-                ]
-            )
-        );
-        return $method;
-    }
-
-    protected function makeSnippetAdderForCollection(Property $prop): Method {
-
-        $singularPropName = rtrim($prop->getName(), 's');
-
-        $method = new Method('add' . ucfirst($singularPropName));
-
-        $parameter = new Parameter($singularPropName);
-        $parameter->setType($prop->getType());
-        $method->setParameters([$parameter]);
-
-        $method->setReturnType('self');
-        $method->setBody(
-            implode(
-                "\n",
-                [
-                    'if (!$this->' . $prop->getName() . '->contains($' . $singularPropName . ')) {',
-                    '    $this->' . $prop->getName() . '[] = $' . $singularPropName . ';',
-                    '}',
-                    '',
-                    'return $this;',
-                ]
-            )
-        );
-        return $method;
-    }
-
-    protected function makeSnippetRemoverForCollection(Property $prop): Method {
-        $singularPropName = rtrim($prop->getName(), 's');
-
-        $method = new Method('remove' . ucfirst($singularPropName));
-
-        $parameter = new Parameter($singularPropName);
-        $parameter->setType($prop->getType());
-        $method->setParameters([$parameter]);
-
-        $method->setReturnType('self');
-        $method->setBody(
-            implode(
-                "\n",
-                [
-                    '$this->' . $prop->getName() . '->removeElement($' . $singularPropName . ');',
-                    '',
-                    'return $this;',
-                ]
-            )
-        );
-        return $method;
+        throw new RuntimeException('Unknown relation type');
     }
 }

+ 69 - 0
src/Service/Utils/EntityUtils.php

@@ -7,12 +7,20 @@ namespace App\Service\Utils;
 use App\Attribute\BillingSettingDefaultValue;
 use App\Attribute\OrganizationDefaultValue;
 use App\Entity\Access\Access;
+use Doctrine\ORM\EntityManagerInterface;
 
 /**
  * Class EntityUtils : Gestion des valeurs par défauts devant être présentes dans les entités.
  */
 class EntityUtils
 {
+    protected array $entityNamesMappingCache = [];
+
+    public function __construct(
+        private readonly EntityManagerInterface  $entityManager,
+    )
+    {}
+
     /**
      * @throws \ReflectionException
      */
@@ -47,4 +55,65 @@ class EntityUtils
             $entity->{sprintf('set%s', ucfirst($fieldName))}(...[$access->getOrganization()?->getBillingSetting()]);
         }
     }
+
+
+    /**
+     * Extract an entity base name from a fully qualified name
+     * Ex: '\App\Entity\Core\File' => 'File'
+     *
+     * @param string $fullName
+     * @return string
+     */
+    public function getEntityNameFromFullName(string $fullName): string
+    {
+        $parts = explode('\\', $fullName);
+        return array_pop($parts);
+    }
+
+    /**
+     * Find the fully qualified name matching the given entity base name
+     *
+     * Ex: 'File' => '\App\Entity\Core\File'
+     *
+     * @param string $entityName
+     * @return string
+     */
+    public function getFullNameFromEntityName(string $entityName): string
+    {
+        if (empty($this->entityNamesMappingCache)) {
+            $this->populateEntityNamesMappingCache();
+        }
+        if (!isset($this->entityNamesMappingCache[$entityName])) {
+            throw new \LogicException('No entity found for name '.$entityName);
+        }
+        return $this->entityNamesMappingCache[$entityName];
+
+    }
+
+    /**
+     * Populates a cache mapping entity base names to their fully qualified class names.
+     *
+     * @return void
+     */
+    protected function populateEntityNamesMappingCache() : void {
+        $metadata = $this->entityManager->getMetadataFactory()->getAllMetadata();
+        foreach ($metadata as $entityMetadata) {
+            $baseName = $this->getEntityNameFromFullName($entityMetadata->getName());
+            $this->entityNamesMappingCache[$baseName] = $entityMetadata->getName();
+        }
+    }
+
+    /**
+     * Get the namespace from a fully qualified name
+     * Ex: '\App\Entity\Core\File' => '\App\Entity\Core'
+     *
+     * @param string $entityName
+     * @return string|null
+     */
+    public function getNamespaceFromName(string $entityName): ?string
+    {
+        $parts = explode('\\', $entityName);
+        array_pop($parts);
+        return implode('\\', $parts);
+    }
 }