|
|
@@ -0,0 +1,751 @@
|
|
|
+<?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\InverseJoinColumn;
|
|
|
+use Doctrine\ORM\Mapping\JoinColumn;
|
|
|
+use Doctrine\ORM\Mapping\JoinTable;
|
|
|
+use Doctrine\ORM\Mapping\ManyToMany;
|
|
|
+use Doctrine\ORM\Mapping\ManyToOne;
|
|
|
+use Doctrine\ORM\Mapping\OneToMany;
|
|
|
+use Doctrine\ORM\Mapping\OneToOne;
|
|
|
+use Nette\InvalidStateException;
|
|
|
+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;
|
|
|
+
|
|
|
+/**
|
|
|
+ * 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
|
|
|
+ */
|
|
|
+ public function makeSnippets(array $diff): void
|
|
|
+ {
|
|
|
+ $snippetsDir = $this->getSnippetsDir();
|
|
|
+ $this->prepareSnippetsDir($snippetsDir);
|
|
|
+
|
|
|
+ foreach ($diff as $entity => $differences) {
|
|
|
+ if (empty($differences)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ $class = $this->makeSnippetEntityClass($entity);
|
|
|
+
|
|
|
+ $methods = [];
|
|
|
+ $collections = [];
|
|
|
+
|
|
|
+ $expectedFields = [];
|
|
|
+ if (!is_array($differences)) {
|
|
|
+ // New entity
|
|
|
+ foreach ($differences->getExpectedType() as $field => $type) {
|
|
|
+ $expectedFields[$field] = $type;
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ // Existing entity
|
|
|
+ foreach ($differences as $field => $difference) {
|
|
|
+ $expectedFields[$field] = $difference->getExpectedType();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ foreach ($expectedFields as $field => $expectedType) {
|
|
|
+ $prop = is_array($expectedType) ?
|
|
|
+ $this->makeSnippetEntityCollectionProp($field, $expectedType) :
|
|
|
+ $this->makeSnippetEntitySimpleProp($field, $expectedType);
|
|
|
+
|
|
|
+ $class->addMember($prop);
|
|
|
+
|
|
|
+ if ($prop->getType() === Collection::class) {
|
|
|
+ $collections[] = $prop;
|
|
|
+ }
|
|
|
+
|
|
|
+ $methods = [...$methods, ...$this->makeMethodsSnippetForProp($prop)];
|
|
|
+ }
|
|
|
+
|
|
|
+ if ($collections) {
|
|
|
+ $class->addMember(
|
|
|
+ $this->makeSnippetConstructor($collections)
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ foreach ($methods as $method) {
|
|
|
+ $class->addMember($method);
|
|
|
+ }
|
|
|
+
|
|
|
+ $file = $this->makeFileSnippet();
|
|
|
+
|
|
|
+ $namespace = $this->makeNamespaceSnippet($entity);
|
|
|
+
|
|
|
+ if (is_array($differences)) {
|
|
|
+ foreach ($this->getEntityToImportFromRelations($differences) as $use) {
|
|
|
+ $namespace->addUse($use);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ $namespace->add($class);
|
|
|
+ } catch (InvalidStateException $e) {
|
|
|
+ var_dump($e->getMessage());
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ $file->addNamespace($namespace);
|
|
|
+
|
|
|
+ $fileName = $this->getSnippetPath($snippetsDir, $entity);
|
|
|
+
|
|
|
+ $this->writeSnippet($file, $fileName);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Make the getters / setters for the given property.
|
|
|
+ *
|
|
|
+ * @return array<Method>
|
|
|
+ */
|
|
|
+ protected function makeMethodsSnippetForProp(Property $prop): array
|
|
|
+ {
|
|
|
+ $methods = [];
|
|
|
+
|
|
|
+ if ($prop->getType() !== Collection::class) {
|
|
|
+ $methods[] = $this->makeSnippetGetterForProp($prop);
|
|
|
+ $methods[] = $this->makeSnippetSetterForProp($prop);
|
|
|
+ } else {
|
|
|
+ $methods[] = $this->makeSnippetGetterForProp($prop);
|
|
|
+ $methods[] = $this->makeSnippetAdderForCollection($prop);
|
|
|
+ $methods[] = $this->makeSnippetRemoverForCollection($prop);
|
|
|
+ }
|
|
|
+
|
|
|
+ return $methods;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Génère un objet ClassType.
|
|
|
+ *
|
|
|
+ * @see https://api.nette.org/php-generator/master/Nette/PhpGenerator/ClassType.html
|
|
|
+ */
|
|
|
+ protected function makeSnippetEntityClass(string $entity): ClassType
|
|
|
+ {
|
|
|
+ $class = new ClassType($entity);
|
|
|
+
|
|
|
+ $class->setAttributes([
|
|
|
+ new Attribute(ApiResource::class, ['operations' => []]),
|
|
|
+ new Attribute(Entity::class, []),
|
|
|
+ ]);
|
|
|
+
|
|
|
+ return $class;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Retourne le chemin absolu vers le répertoire dans lesquels sont créés les snippets.
|
|
|
+ */
|
|
|
+ protected function getSnippetsDir(): string
|
|
|
+ {
|
|
|
+ return Path::join(Path::getProjectDir(), 'schema_validation_snippets');
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Vide et ajuste les droits du répertoire des snippets.
|
|
|
+ */
|
|
|
+ protected function prepareSnippetsDir(string $snippetsDir): void
|
|
|
+ {
|
|
|
+ if (is_dir($snippetsDir)) {
|
|
|
+ FileUtils::rrmDir($snippetsDir);
|
|
|
+ }
|
|
|
+ mkdir($snippetsDir, 0777, true);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Retourne le chemin absolu du snippet donné.
|
|
|
+ */
|
|
|
+ protected function getSnippetPath(string $snippetsDir, string $entity): string
|
|
|
+ {
|
|
|
+ try {
|
|
|
+ $fullName = $this->entityUtils->getFullNameFromEntityName($entity);
|
|
|
+ } catch (\LogicException) {
|
|
|
+ $fullName = '_NameSpaceNotFound/'.$entity;
|
|
|
+ }
|
|
|
+
|
|
|
+ $relativePath = str_replace('\\', '/', $fullName).'.php';
|
|
|
+
|
|
|
+ return Path::join($snippetsDir, $relativePath);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Créé le fichier du snippet sur le disque.
|
|
|
+ */
|
|
|
+ protected function writeSnippet(PhpFile $phpFile, string $fileName): void
|
|
|
+ {
|
|
|
+ if (!is_dir(dirname($fileName))) {
|
|
|
+ mkdir(dirname($fileName), 0777, true);
|
|
|
+ }
|
|
|
+
|
|
|
+ $printer = new PsrPrinter();
|
|
|
+
|
|
|
+ $content = $printer->printFile($phpFile);
|
|
|
+ $content = $this->postProcessFileContent($content);
|
|
|
+
|
|
|
+ $f = fopen($fileName, 'w+');
|
|
|
+ try {
|
|
|
+ fwrite($f, $content);
|
|
|
+ } finally {
|
|
|
+ fclose($f);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 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;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Obtient le namespace pour l'entité donnée.
|
|
|
+ */
|
|
|
+ protected function getNamespaceValue(string $entity): string
|
|
|
+ {
|
|
|
+ try {
|
|
|
+ $fullQualifiedName = str_contains($entity, '\\') ?
|
|
|
+ $entity :
|
|
|
+ $this->entityUtils->getFullNameFromEntityName($entity);
|
|
|
+
|
|
|
+ return $this->entityUtils->getNamespaceFromName($fullQualifiedName);
|
|
|
+ } catch (\LogicException) {
|
|
|
+ return 'App\\Entity';
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * @param array<string, string|array<string>> $type
|
|
|
+ */
|
|
|
+ protected function getRelationTargetEntityName(array $type): string
|
|
|
+ {
|
|
|
+ $targetEntityName = $this->entityUtils->getEntityNameFromFullName($type['targetEntity']);
|
|
|
+ try {
|
|
|
+ return $this->entityUtils->getFullNameFromEntityName($targetEntityName);
|
|
|
+ } catch (\LogicException) {
|
|
|
+ return 'mixed';
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Construit l'objet PhpFile.
|
|
|
+ *
|
|
|
+ * @see https://api.nette.org/php-generator/master/Nette/PhpGenerator/PhpFile.html
|
|
|
+ */
|
|
|
+ protected function makeFileSnippet(): PhpFile
|
|
|
+ {
|
|
|
+ $file = new PhpFile();
|
|
|
+ $file->addComment('This file is auto-generated.');
|
|
|
+ $file->setStrictTypes();
|
|
|
+
|
|
|
+ return $file;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Construit l'objet PhpNamespace.
|
|
|
+ *
|
|
|
+ * @see https://api.nette.org/php-generator/master/Nette/PhpGenerator/PhpNamespace.html
|
|
|
+ */
|
|
|
+ protected function makeNamespaceSnippet(string $entity): PhpNamespace
|
|
|
+ {
|
|
|
+ $namespaceValue = $this->getNamespaceValue($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');
|
|
|
+
|
|
|
+ return $namespace;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Construit l'objet Property pour le champs 'id' d'une entité.
|
|
|
+ *
|
|
|
+ * @see https://api.nette.org/php-generator/master/Nette/PhpGenerator/Property.html
|
|
|
+ */
|
|
|
+ protected function makeIdPropertySnippet(): Property
|
|
|
+ {
|
|
|
+ $prop = new Property('id');
|
|
|
+ $prop->setPrivate();
|
|
|
+ $prop->setType('int');
|
|
|
+ $prop->addAttribute('ORM\Id');
|
|
|
+ $prop->addAttribute('ORM\Column');
|
|
|
+ $prop->addAttribute('ORM\GeneratedValue');
|
|
|
+
|
|
|
+ return $prop;
|
|
|
+ }
|
|
|
+
|
|
|
+ protected function getPhpTypeFromDoctrineType(string $doctrineType): string
|
|
|
+ {
|
|
|
+ return [
|
|
|
+ 'text' => 'string',
|
|
|
+ 'boolean' => 'bool',
|
|
|
+ 'integer' => 'int',
|
|
|
+ 'datetime' => '?\DateTimeInterface',
|
|
|
+ 'json_array' => 'array',
|
|
|
+ ][$doctrineType] ?? 'mixed';
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Make a Property object for a simple field (not a relation).
|
|
|
+ *
|
|
|
+ * @see https://api.nette.org/php-generator/master/Nette/PhpGenerator/Property.html
|
|
|
+ */
|
|
|
+ protected function makeSnippetEntitySimpleProp(string $name, string $type): Property
|
|
|
+ {
|
|
|
+ if ($name === 'id') {
|
|
|
+ return $this->makeIdPropertySnippet();
|
|
|
+ }
|
|
|
+
|
|
|
+ $php_type = $this->getPhpTypeFromDoctrineType($type);
|
|
|
+
|
|
|
+ $prop = new Property($name);
|
|
|
+ $prop->setProtected();
|
|
|
+ $prop->setType($php_type);
|
|
|
+ $prop->setComment('-- Warning : auto-generated property, checkup the attribute options --');
|
|
|
+
|
|
|
+ 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 array<mixed> $type
|
|
|
+ */
|
|
|
+ protected function makeSnippetEntityCollectionProp(string $name, array $type): Property
|
|
|
+ {
|
|
|
+ $prop = new Property($name);
|
|
|
+ $prop->setProtected();
|
|
|
+ $prop->setType('Collection');
|
|
|
+
|
|
|
+ if (
|
|
|
+ isset($type['type'])
|
|
|
+ && $type['type'] === ClassMetadataInfo::ONE_TO_ONE || $type['type'] === ClassMetadataInfo::MANY_TO_ONE
|
|
|
+ ) {
|
|
|
+ $targetEntityName = $this->getRelationTargetEntityName($type);
|
|
|
+ $prop->setType($targetEntityName);
|
|
|
+ } else {
|
|
|
+ $prop->setType(Collection::class);
|
|
|
+ }
|
|
|
+
|
|
|
+ $attributes = [];
|
|
|
+ $attributes[] = $this->makeRelationAttribute($type);
|
|
|
+
|
|
|
+ $joinTable = $this->makeJoinTableAttributes($type);
|
|
|
+ if ($joinTable) {
|
|
|
+ $attributes[] = $joinTable;
|
|
|
+ }
|
|
|
+
|
|
|
+ $attributes = array_merge($attributes, $this->makeJoinColumnAttributes($type));
|
|
|
+
|
|
|
+ $attributes = array_merge($attributes, $this->makeJoinColumnAttributes($type, true));
|
|
|
+
|
|
|
+ $prop->setAttributes($attributes);
|
|
|
+
|
|
|
+ return $prop;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Make the attribute defining the relation (ex: #[ORM\ManyToMany(...)]).
|
|
|
+ *
|
|
|
+ * @param array<string | array<string | array<string>>> $type
|
|
|
+ */
|
|
|
+ protected function makeRelationAttribute(array $type): Attribute
|
|
|
+ {
|
|
|
+ $options = [];
|
|
|
+ if (isset($type['mappedBy'])) {
|
|
|
+ $options['mappedBy'] = $type['mappedBy'];
|
|
|
+ }
|
|
|
+
|
|
|
+ if (isset($type['targetEntity'])) {
|
|
|
+ $options['targetEntity'] = $this->entityUtils->getEntityNameFromFullName($type['targetEntity']).'::class';
|
|
|
+ }
|
|
|
+
|
|
|
+ if (isset($type['cascade'])) {
|
|
|
+ $options['cascade'] = $type['cascade'];
|
|
|
+ }
|
|
|
+
|
|
|
+ if (isset($type['inversedBy'])) {
|
|
|
+ $options['inversedBy'] = $type['inversedBy'];
|
|
|
+ }
|
|
|
+
|
|
|
+ if (isset($type['orphanRemoval']) && ($type['type'] === ClassMetadataInfo::ONE_TO_MANY || $type['type'] === ClassMetadataInfo::MANY_TO_MANY)) {
|
|
|
+ $options['orphanRemoval'] = $type['orphanRemoval'];
|
|
|
+ }
|
|
|
+
|
|
|
+ $relationClassNames = [
|
|
|
+ ClassMetadataInfo::ONE_TO_MANY => OneToMany::class,
|
|
|
+ ClassMetadataInfo::MANY_TO_MANY => ManyToMany::class,
|
|
|
+ ClassMetadataInfo::MANY_TO_ONE => ManyToOne::class,
|
|
|
+ ClassMetadataInfo::ONE_TO_ONE => OneToOne::class,
|
|
|
+ ];
|
|
|
+
|
|
|
+ return new Attribute($relationClassNames[$type['type']], $options);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Make the #[ORM\JoinTable] attribute (if a definition exists).
|
|
|
+ *
|
|
|
+ * @param array<string | array<string | array<string>>> $type
|
|
|
+ */
|
|
|
+ protected function makeJoinTableAttributes(array $type): ?Attribute
|
|
|
+ {
|
|
|
+ if (!isset($type['joinTable'])) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ $options = [];
|
|
|
+ if (isset($type['joinTable']['name']) && $type['joinTable']['name']) {
|
|
|
+ $options['name'] = $type['joinTable']['name'];
|
|
|
+ }
|
|
|
+
|
|
|
+ return new Attribute(JoinTable::class, $options);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Make the #[JoinColumn] attributes, if definitions exists.
|
|
|
+ *
|
|
|
+ * Is `$inverse` is true, make the #[InverseJoinColumn] instead.
|
|
|
+ *
|
|
|
+ * @param array<string | array<string | array<string>>> $type
|
|
|
+ *
|
|
|
+ * @return array<Attribute>
|
|
|
+ */
|
|
|
+ protected function makeJoinColumnAttributes(array $type, bool $inverse = false): array
|
|
|
+ {
|
|
|
+ $key = $inverse ? 'inverseJoinColumns' : 'joinColumns';
|
|
|
+
|
|
|
+ $definition = $type[$key] ?? $type['joinTable'][$key] ?? [];
|
|
|
+ if (empty($definition)) {
|
|
|
+ return [];
|
|
|
+ }
|
|
|
+
|
|
|
+ $attributes = [];
|
|
|
+
|
|
|
+ foreach ($definition as $joinColDefinition) {
|
|
|
+ $options = [];
|
|
|
+
|
|
|
+ if (isset($joinColDefinition['name']) && $joinColDefinition['name'] !== $type['fieldName'].'_id') {
|
|
|
+ $options['name'] = $joinColDefinition['name'];
|
|
|
+ }
|
|
|
+ if (($joinColDefinition['unique'] ?? false) === true) {
|
|
|
+ $options['unique'] = true;
|
|
|
+ }
|
|
|
+ if (($joinColDefinition['nullable'] ?? true) === false) {
|
|
|
+ $options['nullable'] = false;
|
|
|
+ }
|
|
|
+ if (($joinColDefinition['onDelete'] ?? null) !== null) {
|
|
|
+ $options['onDelete'] = $joinColDefinition['onDelete'];
|
|
|
+ }
|
|
|
+ if (($joinColDefinition['columnDefinition'] ?? null) !== null) {
|
|
|
+ $options['columnDefinition'] = $joinColDefinition['columnDefinition'];
|
|
|
+ }
|
|
|
+ if (($joinColDefinition['referencedColumnName'] ?? 'id') !== 'id') {
|
|
|
+ $options['referencedColumnName'] = $joinColDefinition['referencedColumnName'];
|
|
|
+ }
|
|
|
+
|
|
|
+ if (empty($options)) {
|
|
|
+ // Useless attribute
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ $attributes[] = new Attribute(
|
|
|
+ $inverse ? InverseJoinColumn::class : JoinColumn::class,
|
|
|
+ $options
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ return $attributes;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Make the '__construct' method with collections initialization.
|
|
|
+ *
|
|
|
+ * @param array<Property> $collections
|
|
|
+ */
|
|
|
+ protected function makeSnippetConstructor(array $collections): Method
|
|
|
+ {
|
|
|
+ $constructor = new Method('__construct');
|
|
|
+ $constructor->setPublic();
|
|
|
+
|
|
|
+ foreach ($collections as $collection) {
|
|
|
+ $constructor->addBody('$this->'.$collection->getName().' = new ArrayCollection();');
|
|
|
+ }
|
|
|
+
|
|
|
+ return $constructor;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Make a 'getter' method for the given property.
|
|
|
+ */
|
|
|
+ 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.
|
|
|
+ */
|
|
|
+ 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 getTargetEntityNameFromCollectionProp(Property $prop): ?string
|
|
|
+ {
|
|
|
+ if ($prop->getType() !== Collection::class) {
|
|
|
+ throw new \LogicException('The property must be a collection');
|
|
|
+ }
|
|
|
+
|
|
|
+ foreach ($prop->getAttributes() as $attribute) {
|
|
|
+ if (
|
|
|
+ $attribute instanceof Attribute
|
|
|
+ && ($attribute->getName() === OneToMany::class || $attribute->getName() === ManyToMany::class)
|
|
|
+ ) {
|
|
|
+ $targetEntityName = $attribute->getArguments()['targetEntity'];
|
|
|
+ if (!$targetEntityName) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Normalize result (it could be a FQN, or a '::class' notation)
|
|
|
+ $targetEntityName = str_replace('::class', '', $targetEntityName);
|
|
|
+
|
|
|
+ if (!str_contains($targetEntityName, '\\')) {
|
|
|
+ try {
|
|
|
+ $targetEntityName = $this->entityUtils->getFullNameFromEntityName($targetEntityName);
|
|
|
+ } catch (\LogicException) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return $targetEntityName;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ protected function getInverseSetterCallFromCollectionProp(Property $prop, bool $isRemoving = false): ?string
|
|
|
+ {
|
|
|
+ if (
|
|
|
+ $prop->getType() !== Collection::class
|
|
|
+ ) {
|
|
|
+ throw new \LogicException('The property must be a collection');
|
|
|
+ }
|
|
|
+
|
|
|
+ $relationAttr = null;
|
|
|
+ foreach ($prop->getAttributes() as $attribute) {
|
|
|
+ if ($attribute->getName() === OneToMany::class || $attribute->getName() === ManyToMany::class) {
|
|
|
+ $relationAttr = $attribute;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!$relationAttr) {
|
|
|
+ throw new \LogicException('Missing relation attribute for collection property '.$prop->getName());
|
|
|
+ }
|
|
|
+
|
|
|
+ $inversedBy = $relationAttr->getArguments()['inversedBy'] ?? $relationAttr->getArguments()['mappedBy'] ?? null;
|
|
|
+ if (!$inversedBy) {
|
|
|
+ var_dump('Could not determine the inverse prop for collection property '.$prop->getName());
|
|
|
+ $inversedBy = 'XXXX';
|
|
|
+ }
|
|
|
+
|
|
|
+ $attr = $prop->getAttributes()[0];
|
|
|
+
|
|
|
+ if ($attr->getName() === OneToMany::class) {
|
|
|
+ $prefix = 'set';
|
|
|
+ } else {
|
|
|
+ $prefix = $isRemoving ? 'remove' : 'add';
|
|
|
+ $inversedBy = $this->singularize($inversedBy);
|
|
|
+ }
|
|
|
+
|
|
|
+ return
|
|
|
+ $prefix.
|
|
|
+ ucfirst($inversedBy).
|
|
|
+ (($prefix === 'set' && $isRemoving) ? '(null)' : '($this)');
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Make an 'adder' method for the given property.
|
|
|
+ */
|
|
|
+ protected function makeSnippetAdderForCollection(Property $prop): Method
|
|
|
+ {
|
|
|
+ $singularPropName = $this->singularize($prop->getName());
|
|
|
+
|
|
|
+ $method = new Method('add'.ucfirst($singularPropName));
|
|
|
+
|
|
|
+ $targetEntityName = $this->getTargetEntityNameFromCollectionProp($prop);
|
|
|
+
|
|
|
+ $parameter = new Parameter($singularPropName);
|
|
|
+ $parameter->setType($targetEntityName ?? 'mixed');
|
|
|
+ $method->setParameters([$parameter]);
|
|
|
+
|
|
|
+ $inverseSetterCall = $this->getInverseSetterCallFromCollectionProp($prop);
|
|
|
+
|
|
|
+ $method->setReturnType('self');
|
|
|
+ $method->setBody(implode(
|
|
|
+ "\n",
|
|
|
+ [
|
|
|
+ 'if (!$this->'.$prop->getName().'->contains($'.$singularPropName.')) {',
|
|
|
+ ' $this->'.$prop->getName().'[] = $'.$singularPropName.';',
|
|
|
+ ' $'.$singularPropName.'->'.$inverseSetterCall.';',
|
|
|
+ '}',
|
|
|
+ '',
|
|
|
+ 'return $this;',
|
|
|
+ ]));
|
|
|
+
|
|
|
+ return $method;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Make a 'remover' method for the given property.
|
|
|
+ */
|
|
|
+ protected function makeSnippetRemoverForCollection(Property $prop): Method
|
|
|
+ {
|
|
|
+ $singularPropName = $this->singularize($prop->getName());
|
|
|
+
|
|
|
+ $method = new Method('remove'.ucfirst($singularPropName));
|
|
|
+
|
|
|
+ $targetEntityName = $this->getTargetEntityNameFromCollectionProp($prop);
|
|
|
+
|
|
|
+ $parameter = new Parameter($singularPropName);
|
|
|
+ $parameter->setType($targetEntityName ?? 'mixed');
|
|
|
+ $method->setParameters([$parameter]);
|
|
|
+
|
|
|
+ $inverseSetterCall = $this->getInverseSetterCallFromCollectionProp($prop, true);
|
|
|
+
|
|
|
+ $method->setReturnType('self');
|
|
|
+ $method->setBody(
|
|
|
+ implode(
|
|
|
+ "\n",
|
|
|
+ [
|
|
|
+ 'if ($this->'.$prop->getName().'->removeElement($'.$singularPropName.')) {',
|
|
|
+ ' $'.$singularPropName.'->'.$inverseSetterCall.';',
|
|
|
+ '}',
|
|
|
+ '',
|
|
|
+ 'return $this;',
|
|
|
+ ]
|
|
|
+ )
|
|
|
+ );
|
|
|
+
|
|
|
+ return $method;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Perform some post-fixes on the file content.
|
|
|
+ */
|
|
|
+ protected function postProcessFileContent(string $content): string
|
|
|
+ {
|
|
|
+ return preg_replace("/targetEntity: '(\w+)::class'/", 'targetEntity: $1::class', $content);
|
|
|
+ }
|
|
|
+
|
|
|
+ protected function singularize(string $name): string
|
|
|
+ {
|
|
|
+ $exceptions = ['access'];
|
|
|
+ if (in_array(strtolower($name), $exceptions)) {
|
|
|
+ return $name;
|
|
|
+ }
|
|
|
+
|
|
|
+ return preg_replace('/s$/', '', $name);
|
|
|
+ }
|
|
|
+}
|