|
|
@@ -0,0 +1,243 @@
|
|
|
+<?php
|
|
|
+declare(strict_types=1);
|
|
|
+
|
|
|
+namespace App\Service\Doctrine\SchemaValidation;
|
|
|
+
|
|
|
+use App\Service\ApiLegacy\ApiLegacyRequestService;
|
|
|
+use Doctrine\ORM\EntityManagerInterface;
|
|
|
+use Doctrine\ORM\Mapping\ClassMetadataInfo;
|
|
|
+use Doctrine\ORM\Mapping\MappingException;
|
|
|
+use RuntimeException;
|
|
|
+use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
|
|
|
+use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface;
|
|
|
+use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface;
|
|
|
+use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
|
|
|
+
|
|
|
+/**
|
|
|
+ * Validation du schéma Doctrine par comparaison aux entités en production sur la V1
|
|
|
+ *
|
|
|
+ * — À supprimer lorsque la migration sera achevée —
|
|
|
+ *
|
|
|
+ */
|
|
|
+class SchemaValidationService
|
|
|
+{
|
|
|
+ public function __construct(
|
|
|
+ private readonly EntityManagerInterface $entityManager,
|
|
|
+ private readonly ApiLegacyRequestService $apiLegacyRequestService,
|
|
|
+ )
|
|
|
+ {}
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Compare the V2 doctrine schema to the one in V1, and return a list of differences,
|
|
|
+ * of the form:
|
|
|
+ *
|
|
|
+ * [<entity> → Difference | [<field> → Difference]]
|
|
|
+ *
|
|
|
+ * @return array
|
|
|
+ * @throws ClientExceptionInterface
|
|
|
+ * @throws MappingException
|
|
|
+ * @throws RedirectionExceptionInterface
|
|
|
+ * @throws ServerExceptionInterface
|
|
|
+ * @throws TransportExceptionInterface
|
|
|
+ */
|
|
|
+ public function validateSchema(): array
|
|
|
+ {
|
|
|
+ $schemaV1 = $this->getV1Schema();
|
|
|
+ $schemaV2 = $this->getV2Schema();
|
|
|
+
|
|
|
+ $diff = $this->getDiff($schemaV1, $schemaV2);
|
|
|
+
|
|
|
+ foreach ($diff['Organization'] as $field => $diffEntry) {
|
|
|
+ var_dump($field . " : " . $diffEntry->value);
|
|
|
+ }
|
|
|
+
|
|
|
+ return $diff;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Retrieve the V2 schema
|
|
|
+ *
|
|
|
+ * @return array<string, array<string | array<string|int>>
|
|
|
+ * @throws MappingException
|
|
|
+ */
|
|
|
+ protected function getV2Schema(): array
|
|
|
+ {
|
|
|
+ $metadata = $this->entityManager->getMetadataFactory()->getAllMetadata();
|
|
|
+ $schema = [];
|
|
|
+
|
|
|
+ foreach ($metadata as $entityMetadata) {
|
|
|
+ $schema[$entityMetadata->getTableName()] = [];
|
|
|
+
|
|
|
+ foreach ($entityMetadata->getFieldNames() as $field) {
|
|
|
+ $schema[$entityMetadata->getTableName()][$field] = $entityMetadata->getTypeOfField($field);
|
|
|
+ }
|
|
|
+
|
|
|
+ foreach ($entityMetadata->getAssociationNames() as $association) {
|
|
|
+ $schema[$entityMetadata->getTableName()][$association] = $entityMetadata->getAssociationMapping($association);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return $schema;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Retrieve the V1 schema
|
|
|
+ *
|
|
|
+ * @return array<string, array<string | array<string|int>>
|
|
|
+ * @throws ClientExceptionInterface
|
|
|
+ * @throws RedirectionExceptionInterface
|
|
|
+ * @throws ServerExceptionInterface
|
|
|
+ * @throws TransportExceptionInterface
|
|
|
+ */
|
|
|
+ protected function getV1Schema(): array {
|
|
|
+ $response = $this->apiLegacyRequestService->get('/_internal/doctrine/schema');
|
|
|
+
|
|
|
+ return json_decode($response->getContent(), true);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Get a list of differences between V1 and V2 doctrine schemas
|
|
|
+ *
|
|
|
+ * @param array<string, array<string | array<string|int>> $schemaV1
|
|
|
+ * @param array<string, array<string| array<string|int>>> $schemaV2
|
|
|
+ * @return array<string, Difference | array<Difference>>
|
|
|
+ */
|
|
|
+ protected function getDiff(array $schemaV1, array $schemaV2): array {
|
|
|
+ $diff = [
|
|
|
+ ];
|
|
|
+
|
|
|
+ foreach ($schemaV1 as $entity => $fields) {
|
|
|
+
|
|
|
+ if (!$this->isEntityInSchema($schemaV2, $entity)) {
|
|
|
+ // L'entité n'existe pas en V2
|
|
|
+ $diff[$entity] = new Difference(DiffTypeEnum::MISSING_ENTITY, "Entity $entity is missing in V2");
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ $diff[$entity] = [];
|
|
|
+
|
|
|
+ foreach ($fields as $field => $fieldTypeV1) {
|
|
|
+
|
|
|
+ if (!$this->isPropertyInSchema($schemaV2, $entity, $field)) {
|
|
|
+ // Le champ n'existe pas en V2
|
|
|
+ $diff[$entity][$field] = new Difference(DiffTypeEnum::MISSING_PROPERTY, "Property $field is missing in V2");
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ $fieldTypeV2 = $schemaV2[$entity][$field];
|
|
|
+
|
|
|
+ // Si ce champ n'est pas une relation
|
|
|
+ if (!$this->isRelationField($schemaV1, $entity, $field)) {
|
|
|
+ // Le champ n'est pas une relation en V1
|
|
|
+ if ($fieldTypeV2 !== $fieldTypeV1) {
|
|
|
+ // Le champ a un type différent en V2
|
|
|
+ $diff[$entity][$field] = new Difference(DiffTypeEnum::DIFFERENT_TYPE, "Property $field has a different type (V1: $fieldTypeV1, V2: $fieldTypeV2)");
|
|
|
+ }
|
|
|
+ } elseif (!$this->isRelationField($schemaV2, $entity, $field)) {
|
|
|
+ // Le champ est une relation en V1 mais pas en V2
|
|
|
+ $diff[$entity][$field] = new Difference(DiffTypeEnum::DIFFERENT_TYPE, "Property $field is a relation in V1 but not in V2");
|
|
|
+ } else {
|
|
|
+ // Le champ est une relation dans les deux schémas, on compare leurs configurations
|
|
|
+ $difference = $this->getRelationDiff($fieldTypeV1, $fieldTypeV2);
|
|
|
+ if ($difference) {
|
|
|
+ $diff[$entity][$field] = $difference;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return $diff;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Returns true if the given entity name exists in the doctrine schema
|
|
|
+ *
|
|
|
+ * @param array<string, array<string | array<string|int>> $schema
|
|
|
+ * @param string $entity
|
|
|
+ * @return bool
|
|
|
+ */
|
|
|
+ protected function isEntityInSchema(array $schema, string $entity): bool {
|
|
|
+ return isset($schema[$entity]);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Returns true if the given property name exists in the doctrine schema under this entity.
|
|
|
+ *
|
|
|
+ * @param array<string, array<string | array<string|int>> $schema
|
|
|
+ * @param string $entity
|
|
|
+ * @param string $property
|
|
|
+ * @return bool
|
|
|
+ */
|
|
|
+ protected function isPropertyInSchema(array $schema, string $entity, string $property): bool {
|
|
|
+ return isset($schema[$entity][$property]);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Is the given field is a relation field.
|
|
|
+ *
|
|
|
+ * @param array<string, array<string | array<string|int>> $schema
|
|
|
+ * @param string $entity
|
|
|
+ * @param string $relation
|
|
|
+ * @return bool
|
|
|
+ */
|
|
|
+ protected function isRelationField(array $schema, string $entity, string $relation): bool {
|
|
|
+ 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.
|
|
|
+ *
|
|
|
+ * @param array<string, string> $relationReference
|
|
|
+ * @param array<string, string> $relationCompared
|
|
|
+ * @return Difference|null
|
|
|
+ */
|
|
|
+ protected function getRelationDiff(array $relationReference, array $relationCompared): Difference | null
|
|
|
+ {
|
|
|
+ if ($relationReference['type'] !== $relationCompared['type']) {
|
|
|
+ return new Difference(
|
|
|
+ DiffTypeEnum::DIFFERENT_RELATION_TYPE,
|
|
|
+ "Relation type is different : {$this->getRelationTypeLabel($relationReference)} !== {$this->getRelationTypeLabel($relationCompared)}"
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ if (
|
|
|
+ $relationReference['targetEntity'] !== $relationCompared['targetEntity']
|
|
|
+ ) {
|
|
|
+ return new Difference(
|
|
|
+ DiffTypeEnum::DIFFERENT_RELATION_CONFIGURATION,
|
|
|
+ "Relation configuration is different (targetEntity)"
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ if (
|
|
|
+ $relationReference['mappedBy'] !== $relationCompared['mappedBy']
|
|
|
+ ) {
|
|
|
+ return new Difference(
|
|
|
+ DiffTypeEnum::DIFFERENT_RELATION_CONFIGURATION,
|
|
|
+ "Relation configuration is different (mappedBy)"
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+}
|