> $diff * @return void */ 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 = []; $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); $methods = [...$methods, ...$this->makeMethodsSnippetForProp($prop)]; } 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); } } $namespace->add($class); $file->addNamespace($namespace); $fileName = $this->getSnippetPath($snippetsDir, $entity); $this->writeSnippet($file, $fileName); } } /** * Make the getters / setters for the given property * * @param Property $prop * @return array */ protected function makeMethodsSnippetForProp(Property $prop): array { $methods = []; if ($prop->getType() !== 'Collection') { $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 * * @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; } /** * Retourne le chemin absolu vers le répertoire dans lesquels sont créés les snippets * @return string */ protected function getSnippetsDir(): string { return Path::join(Path::getProjectDir(), 'schema_validation_snippets'); } /** * Vide et ajuste les droits du répertoire des snippets * * @param string $snippetsDir * @return void */ protected function prepareSnippetsDir(string $snippetsDir): void { if (is_dir($snippetsDir)) { FileUtils::rrmDir($snippetsDir); } mkdir($snippetsDir, 0777, true); } /** * Retourne le chemin absolu du snippet donné * * @param string $snippetsDir * @param string $entity * @return string */ 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 * * @param PhpFile $phpFile * @param string $fileName * @return void */ 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 $differences * @return array */ 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 * * @param string $entity * @return string */ protected function getNamespaceValue(string $entity): string { return $this->entityUtils->getNamespaceFromName($entity) ?? ('App\\Entity\\' . $entity); } /** * Construit l'objet PhpFile * @see https://api.nette.org/php-generator/master/Nette/PhpGenerator/PhpFile.html * * @return PhpFile */ 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 * * @param string $entity * @return PhpNamespace */ 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 * * @return Property */ 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 * * @param string $name * @param string $type * @return Property */ protected function makeSnippetEntitySimpleProp(string $name, string $type): Property { $php_type = $this->getPhpTypeFromDoctrineType($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 ( isset($type['type']) && $type['type'] === ClassMetadataInfo::ONE_TO_ONE || $type['type'] === ClassMetadataInfo::MANY_TO_ONE ) { $targetEntityName = $this->entityUtils->getEntityNameFromFullName($type['targetEntity']); try { $newType = $this->entityUtils->getFullNameFromEntityName($targetEntityName); } catch (\LogicException) { $newType = 'mixed'; } $prop->setType($newType); } else { $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']; } $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, ]; $prop->addAttribute($relationClassNames[$type['type']], $options); if ($type['type'] === ClassMetadataInfo::MANY_TO_ONE) { $prop->addAttribute(JoinColumn::class, ['referencedColumnName' => 'id', 'nullable' => false, 'onDelete' => 'SET NULL']); } 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; } /** * Perform some post-fixes on the file content * * @param string $content * @return string */ protected function postProcessFileContent(string $content): string { return preg_replace("/targetEntity: '(\w+)::class'/", "targetEntity: $1::class", $content); } }