Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 80
0.00% covered (danger)
0.00%
0 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
SchemaSnapshotService
0.00% covered (danger)
0.00%
0 / 80
0.00% covered (danger)
0.00%
0 / 6
306
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 take
0.00% covered (danger)
0.00%
0 / 25
0.00% covered (danger)
0.00%
0 / 1
6
 diff
0.00% covered (danger)
0.00%
0 / 29
0.00% covered (danger)
0.00%
0 / 1
72
 diffColumns
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
6
 load
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 getRanMigrations
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace App\Modules\Updater\Services\Core;
4
5class SchemaSnapshotService
6{
7    protected string $snapshotPath;
8
9    public function __construct()
10    {
11        $this->snapshotPath = WRITEPATH . 'updates/snapshots/';
12
13        if (!is_dir($this->snapshotPath)) {
14            mkdir($this->snapshotPath, 0755, true);
15        }
16    }
17
18    // =========================================================
19    // Prend un snapshot de la structure DB
20    // =========================================================
21
22    public function take(string $label): string
23    {
24        $db     = \Config\Database::connect();
25        $tables = $db->listTables();
26
27        $snapshot = [
28            'label'      => $label,
29            'taken_at'   => date('Y-m-d H:i:s'),
30            'tables'     => [],
31            'migrations' => $this->getRanMigrations(),
32        ];
33
34        foreach ($tables as $table) {
35            $fields = $db->getFieldData($table);
36
37            $snapshot['tables'][$table] = array_map(fn($f) => [
38                'name'       => $f->name,
39                'type'       => $f->type,
40                'max_length' => $f->max_length,
41                'nullable'   => $f->nullable ?? null,
42                'default'    => $f->default ?? null,
43                'primary_key'=> $f->primary_key ?? false,
44            ], $fields);
45        }
46
47        $path = $this->snapshotPath . $label . '_' . date('Ymd_His') . '.json';
48
49        file_put_contents(
50            $path,
51            json_encode($snapshot, JSON_PRETTY_PRINT),
52            LOCK_EX
53        );
54
55        return $path;
56    }
57
58    // =========================================================
59    // Compare deux snapshots
60    // =========================================================
61
62    public function diff(string $beforeLabel, string $afterLabel): array
63    {
64        $before = $this->load($beforeLabel);
65        $after  = $this->load($afterLabel);
66
67        if (!$before || !$after) {
68            return [];
69        }
70
71        $addedTables   = [];
72        $modifiedTables = [];
73        $deletedTables  = [];
74
75        // Tables ajoutées
76        foreach ($after['tables'] as $table => $fields) {
77            if (!isset($before['tables'][$table])) {
78                $addedTables[] = $table;
79            } elseif (
80                json_encode($before['tables'][$table]) !==
81                json_encode($after['tables'][$table])
82            ) {
83                // Colonnes modifiées
84                $modifiedTables[$table] = $this->diffColumns(
85                    $before['tables'][$table],
86                    $after['tables'][$table]
87                );
88            }
89        }
90
91        // Tables supprimées
92        foreach ($before['tables'] as $table => $fields) {
93            if (!isset($after['tables'][$table])) {
94                $deletedTables[] = $table;
95            }
96        }
97
98        // Nouvelles migrations exécutées
99        $newMigrations = array_diff(
100            array_column($after['migrations'], 'version'),
101            array_column($before['migrations'], 'version')
102        );
103
104        return [
105            'added_tables'    => $addedTables,
106            'modified_tables' => $modifiedTables,
107            'deleted_tables'  => $deletedTables,
108            'new_migrations'  => array_values($newMigrations),
109        ];
110    }
111
112    // =========================================================
113    // HELPERS
114    // =========================================================
115
116    protected function diffColumns(array $before, array $after): array
117    {
118        $beforeByName = array_column($before, null, 'name');
119        $afterByName  = array_column($after, null, 'name');
120
121        return [
122            'added'   => array_keys(array_diff_key($afterByName, $beforeByName)),
123            'removed' => array_keys(array_diff_key($beforeByName, $afterByName)),
124            'changed' => array_keys(array_filter(
125                $afterByName,
126                fn($field, $name) => isset($beforeByName[$name])
127                    && json_encode($beforeByName[$name]) !== json_encode($field),
128                ARRAY_FILTER_USE_BOTH
129            )),
130        ];
131    }
132
133    protected function load(string $label): ?array
134    {
135        $files = glob($this->snapshotPath . $label . '_*.json');
136
137        if (empty($files)) {
138            return null;
139        }
140
141        // Prend le plus récent
142        usort($files, fn($a, $b) => filemtime($b) <=> filemtime($a));
143
144        return json_decode(file_get_contents($files[0]), true);
145    }
146
147    protected function getRanMigrations(): array
148    {
149        return \Config\Database::connect()
150            ->table('migrations')
151            ->select('version, batch')
152            ->orderBy('version', 'ASC')
153            ->get()
154            ->getResultArray();
155    }
156}