|
|
@@ -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');
|
|
|
}
|
|
|
}
|