浏览代码

finalize ot:db:check command

Olivier Massot 4 月之前
父节点
当前提交
7e51f115eb

+ 174 - 0
src/Commands/Doctrine/OtDbCheckCommand.php

@@ -0,0 +1,174 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Commands\Doctrine;
+
+use Doctrine\DBAL\Connection;
+use Doctrine\ORM\EntityManagerInterface;
+use Doctrine\ORM\Tools\SchemaValidator;
+use Doctrine\ORM\Tools\SchemaTool;
+use Psr\Log\LoggerInterface;
+use Symfony\Component\Console\Attribute\AsCommand;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\Console\Style\SymfonyStyle;
+use Symfony\Contracts\Service\Attribute\Required;
+
+/**
+ * Vérifie l'intégrité de la base de données par rapport aux entités du projet.
+ *
+ * Cette commande permet de vérifier qu'un dump de la base de données qui a été
+ * remonté sur un serveur de test est intègre. Elle effectue les vérifications suivantes :
+ *
+ * 1. Vérification de la connexion à la base de données
+ * 2. Vérification du schéma de la base de données par rapport aux définitions des entités
+ *    - Validation des mappings des entités
+ *    - Vérification de la synchronisation du schéma de la base de données avec les entités
+ * 3. Vérification que les tables contiennent des données
+ *
+ * Utilisation :
+ *   php bin/console ot:db:check                  # Vérification standard
+ *   php bin/console ot:db:check --verbose        # Affiche des informations détaillées, y compris les requêtes SQL nécessaires pour mettre à jour le schéma
+ *
+ * Types de différences détectées :
+ *   - SCHEMA_DIFF : Différence de schéma (colonnes, types, etc.)
+ *   - MISSING_TABLE : Table manquante dans la base de données
+ *   - EXTRA_TABLE : Table supplémentaire dans la base de données
+ *
+ * Codes de retour :
+ *   - 0 (SUCCESS) : La base de données est intègre
+ *   - 1 (FAILURE) : Des problèmes ont été détectés dans la base de données
+ */
+#[AsCommand(
+    name: 'ot:db:check',
+    description: 'Vérifie l\'intégrité de la base de données par rapport aux entités du projet'
+)]
+class OtDbCheckCommand extends Command
+{
+    private LoggerInterface $logger;
+    const IGNORE_EMPTY_TABLES = [
+        'messenger_messages',
+        'enqueue',
+        'zzz_.*',
+        'CycleByEducation', // Table a priori non utilisée
+        'educationalproject_file',
+        'Odyssee',
+        'PeriodNotation', // Table a priori non utilisée
+        'Presence',
+        'tag_accessWish',
+        'tag_control',
+        'tag_educationNotation',
+        'tag_educationStudent',
+        'tag_repair'
+    ];
+
+    public function __construct(
+        private readonly EntityManagerInterface $entityManager,
+    ) {
+        parent::__construct();
+    }
+
+    /** @noinspection PhpUnused */
+    #[Required]
+    public function setLoggerInterface(LoggerInterface $cronLogger): void
+    {
+        $this->logger = $cronLogger;
+    }
+
+    protected function execute(InputInterface $input, OutputInterface $output): int
+    {
+        $this->logger->info('Démarrage de la vérification de l\'intégrité de la base de données');
+
+        $this->logger->info('1. Vérification de la connexion à la base de données');
+        try {
+            $connection = $this->getDbConnection();
+        } catch (\Exception $e) {
+            $this->logger->error($e->getMessage());
+            return Command::FAILURE;
+        }
+
+        $this->logger->info('2. Vérification de l\'intégrité des données');
+        try {
+            $schemaManager = $connection->createSchemaManager();
+            $tables = $schemaManager->listTableNames();
+            $this->logger->debug(sprintf('Nombre total de tables: %d', count($tables)));
+
+            $emptyTables = [];
+
+            foreach ($tables as $table) {
+                try {
+                    if (preg_match('/^' . implode('|', self::IGNORE_EMPTY_TABLES) . '$/', $table)) {
+                        continue;
+                    }
+
+                    $count = (int) $connection->executeQuery("SELECT COUNT(*) FROM {$table}")->fetchOne();
+
+                    if (!$count > 0) {
+                        $emptyTables[] = $table;
+                    }
+
+                    $this->logger->debug(sprintf('Table %s: %d enregistrements', $table, $count));
+                } catch (\Exception $tableException) {
+                    $this->logger->error(sprintf('Impossible de vérifier la table %s: %s', $table, $tableException->getMessage()));
+                    return Command::FAILURE;
+                }
+            }
+
+            if (count($emptyTables) > 0) {
+                $this->logger->error('%d tables vides détectées sur %d tables au total');
+                $this->logger->error('Tables vides:');
+                foreach ($emptyTables as $table) {
+                    $this->logger->error('- ' . $table);
+                }
+                return Command::FAILURE;
+            } else {
+                $this->logger->info('Toutes les tables sont présentes et contiennent des données');
+            }
+        } catch (\Exception $e) {
+            $this->logger->error('Erreur lors de la vérification de l\'intégrité des données: ' . $e->getMessage());
+            $this->logger->debug('Stack trace: ' . $e->getTraceAsString());
+            return Command::FAILURE;
+        }
+
+        return Command::SUCCESS;
+    }
+
+    /**
+     * Establishes a connection to the database.
+     *
+     * @return Connection The active database connection.
+     * @throws \RuntimeException If the connection could not be established
+     *                           or if an error occurs during the process.
+     */
+    private function getDbConnection(): Connection
+    {
+        try {
+            /** @var Connection $connection */
+            $connection = $this->entityManager->getConnection();
+            $connection->connect();
+
+            if ($connection->isConnected()) {
+                $this->logger->info('Connexion à la base de données établie avec succès');
+
+                $params = $connection->getParams();
+                $this->logger->debug(sprintf(
+                    'Paramètres de connexion: Driver=%s, Host=%s, Port=%s, Database=%s, User=%s',
+                    $params['driver'] ?? 'N/A',
+                    $params['host'] ?? 'N/A',
+                    $params['port'] ?? 'N/A',
+                    $params['dbname'] ?? 'N/A',
+                    $params['user'] ?? 'N/A'
+                ));
+            } else {
+                throw new \RuntimeException('Impossible de se connecter à la base de données');
+            }
+        } catch (\Exception $e) {
+            throw new \RuntimeException('Erreur lors de la connexion à la base de données: ' . $e->getMessage());
+        }
+
+        return $connection;
+    }
+}

+ 0 - 209
src/Commands/PostUpgrade/V0_2/PostUpgradeCommand.php

@@ -1,209 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace App\Commands\PostUpgrade\V0_2;
-
-use Psr\Log\LoggerInterface;
-use Symfony\Component\Console\Attribute\AsCommand;
-use Symfony\Component\Console\Command\Command;
-use Symfony\Component\Console\Input\InputInterface;
-use Symfony\Component\Console\Output\OutputInterface;
-
-#[AsCommand(
-    name: 'ot:upgrade:0.2',
-    description: 'Execute the post-upgrade scripts for Ap2i v0.2'
-)]
-class PostUpgradeCommand extends Command
-{
-    public const TARGETED_VERSION = '0.2';
-
-    public function __construct(private LoggerInterface $logger)
-    {
-        parent::__construct();
-    }
-
-    protected function configure(): void
-    {
-    }
-
-    /**
-     * @throws \Exception
-     */
-    protected function execute(InputInterface $input, OutputInterface $output): int
-    {
-        $this->logger->info('Run post-upgrade scripts for version '.self::TARGETED_VERSION);
-
-        $this->populateSubdomains();
-        $this->genEventsUuid();
-        $this->updateFilesStatuses();
-
-        $output->writeln('Post-upgrade operations successfully executed');
-
-        return Command::SUCCESS;
-    }
-
-    private function getOpentalentConnexion(): \PDO
-    {
-        $dbUrl = $_ENV['DATABASE_URL'];
-        $matches = [];
-        preg_match(
-            "/^mysql:\/\/(\w+):([^\s@]+)@([\w\-]+):(\d+)\/(\w+)/",
-            $dbUrl,
-            $matches
-        );
-        [$dbUser, $dbPwd, $dbHost, $dbPort, $dbName] = array_slice($matches, 1);
-
-        $opentalentCnn = new \PDO(
-            'mysql:host='.$dbHost.';dbname='.$dbName,
-            $dbUser,
-            $dbPwd,
-            [\PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8']);
-        $opentalentCnn->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
-
-        return $opentalentCnn;
-    }
-
-    /**
-     * Populate the new Subdomain table.
-     *
-     * @throws \Exception
-     */
-    private function populateSubdomains(): void
-    {
-        $opentalentCnn = $this->getOpentalentConnexion();
-
-        $opentalentCnn->beginTransaction();
-
-        $openassosCnn = new \PDO(
-            'mysql:host=prod-front;dbname=openassos',
-            'dbcloner',
-            'wWZ4hYcrmHLW2mUK',
-            [\PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8']);
-
-        try {
-            $stmt = $opentalentCnn->query('select count(*) from opentalent.Subdomain;');
-            if ($stmt->fetchColumn(0)[0] > 0) {
-                throw new \RuntimeException('Subdomain table is not empty');
-            }
-
-            $this->logger->info('Populate with reserved subdomains');
-            $reservedSubdomains = [
-                'app', 'my', 'api', 'ap2i', 'assistance', 'local', 'ressources', 'logs', 'stats', 'support', 'preprod',
-                'test', 'admin', 'statistiques', 'drive', 'cloud', 'git', 'frames', 'v6', 'v59', 'www', 'myadmin',
-            ];
-            foreach ($reservedSubdomains as $reserved) {
-                $sql = "insert into opentalent.Subdomain (organization_id, subdomain, active)
-                    values (13, '".$reserved."', 0);";
-                $opentalentCnn->query($sql);
-            }
-
-            $this->logger->info('Populate Subdomain table from openassos.sys_domain');
-
-            $sql = "SELECT d.pid, REGEXP_REPLACE(d.domainName, '^(.+)\\\\.opentalent\\\\.fr$', '\\\\1')
-                    FROM openassos.sys_domain d
-                    where d.domainName like '%.opentalent.fr';";
-            $statement = $openassosCnn->query($sql);
-
-            foreach ($statement->fetchAll() as $row) {
-                [$cmsId, $subdomain] = $row;
-                if (!empty($subdomain) and is_numeric($cmsId)) {
-                    $sql = "INSERT INTO opentalent.Subdomain (organization_id, subdomain)
-                        SELECT o.id, '".$subdomain."'
-                        from opentalent.Organization o 
-                        where o.cmsId = ".$cmsId.';';
-                    $opentalentCnn->query($sql);
-                }
-            }
-
-            $sql = "delete
-                    from opentalent.Subdomain
-                    where subdomain REGEXP '^(.*)\\\\.(.*)$'
-                    and REGEXP_REPLACE(subdomain, '\\\\.', '-') in (select subdomain from opentalent.Subdomain);";
-            $opentalentCnn->query($sql);
-
-            $sql = "update opentalent.Subdomain
-                    set subdomain = REGEXP_REPLACE(subdomain, '\\\\.', '-')
-                    where subdomain REGEXP '^(.*)\\\\.(.*)$';";
-            $opentalentCnn->query($sql);
-
-            $this->logger->info('Complete with subdomains from Parameters table');
-            $sql = 'insert into opentalent.Subdomain (organization_id, subdomain)
-                    select distinct o.id, p.subDomain
-                    from opentalent.Parameters p
-                    inner join opentalent.Organization o on o.parameters_id = p.id
-                    left join opentalent.Subdomain s on s.organization_id = o.id
-                    where p.subDomain is not null and not p.subDomain in (select subdomain from opentalent.Subdomain);';
-            $opentalentCnn->query($sql);
-
-            $this->logger->info('Set the current subdomains');
-            $sql = 'update opentalent.Subdomain s set s.active = false;';
-            $opentalentCnn->query($sql);
-
-            $sql = 'update opentalent.Subdomain s
-                    inner join opentalent.Organization o on o.id = s.organization_id
-                    inner join opentalent.Parameters p on p.id = o.parameters_id and s.subdomain = p.subDomain
-                    set s.active = true;';
-            $opentalentCnn->query($sql);
-
-            $this->logger->info('Set the custom domains');
-            $sql = "update opentalent.Parameters
-                    set customDomain = otherWebsite
-                    where otherWebsite not like '%.opentalent.fr'";
-            $opentalentCnn->query($sql);
-
-            $opentalentCnn->commit();
-            $this->logger->info('Subdomain table was successfully populated');
-        } catch (\Exception $e) {
-            $opentalentCnn->rollBack();
-            $this->logger->error((string) $e);
-            $this->logger->critical('Error while populating the subdomains, abort and rollback');
-        }
-    }
-
-    private function genEventsUuid(): void
-    {
-        $opentalentCnn = $this->getOpentalentConnexion();
-
-        $opentalentCnn->beginTransaction();
-
-        try {
-            $this->logger->info('Generate events uuids');
-
-            // Generating Uuid1 with native mysql function
-            $sql = 'update opentalent.Booking set uuid = UUID();';
-            $opentalentCnn->query($sql);
-
-            $sql = 'update opentalent.AwinProduct set uuid = UUID();';
-            $opentalentCnn->query($sql);
-
-            $opentalentCnn->commit();
-            $this->logger->info('Events uuid were successfully generated');
-        } catch (\Exception $e) {
-            $opentalentCnn->rollBack();
-            $this->logger->error((string) $e);
-            $this->logger->critical('Error while generating events uuids, abort and rollback');
-        }
-    }
-
-    private function updateFilesStatuses(): void
-    {
-        $opentalentCnn = $this->getOpentalentConnexion();
-
-        $opentalentCnn->beginTransaction();
-
-        try {
-            $this->logger->info('Update file statuses');
-
-            $sql = "update opentalent.File set status='READY', host='api';";
-            $opentalentCnn->query($sql);
-
-            $opentalentCnn->commit();
-            $this->logger->info('Files statuses were successfully updated');
-        } catch (\Exception $e) {
-            $opentalentCnn->rollBack();
-            $this->logger->error((string) $e);
-            $this->logger->critical('Error while updating file statuses, abort and rollback');
-        }
-    }
-}

+ 1 - 1
src/Entity/Core/Categories.php

@@ -17,7 +17,7 @@ use Doctrine\ORM\Mapping as ORM;
 class Categories
 {
     #[ORM\Id]
-    #[ORM\Column(type: 'mediumint', options: ['unsigned' => true])]
+    #[ORM\Column(type: 'integer', options: ['unsigned' => true])]
     #[ORM\GeneratedValue]
     private ?int $id = null;
 

+ 1 - 1
src/Entity/Core/City.php

@@ -18,7 +18,7 @@ use Doctrine\ORM\Mapping as ORM;
 class City
 {
     #[ORM\Id]
-    #[ORM\Column(type: 'mediumint', options: ['unsigned' => true])]
+    #[ORM\Column]
     #[ORM\GeneratedValue]
     private ?int $id = null;