1+ <?php
2+
3+ declare (strict_types=1 );
4+
5+ namespace MaplePHP \Core \Routing \Migrations ;
6+
7+ use RuntimeException ;
8+ use Psr \Http \Message \ResponseInterface ;
9+ use Doctrine \DBAL \Schema \Schema ;
10+ use Doctrine \DBAL \Schema \AbstractSchemaManager ;
11+ use MaplePHP \Core \App ;
12+ use MaplePHP \Core \Support \Database \Migrations ;
13+ use MaplePHP \Core \Routing \DefaultShellController ;
14+ use MaplePHP \Core \Support \Database \DB ;
15+
16+ class MigrateController extends DefaultShellController
17+ {
18+ private ?AbstractSchemaManager $ schemaManager = null ;
19+
20+ /**
21+ * Run ALL migrations (up), or a single one if --name is provided.
22+ */
23+ public function index (ResponseInterface $ response ): ResponseInterface
24+ {
25+ if (!$ this ->confirm ("Are you sure you want to run ALL migrations? " )) {
26+ return $ response ;
27+ }
28+
29+ $ this ->runDirection ('up ' );
30+
31+ return $ response ;
32+ }
33+
34+ /**
35+ * Run migrations up, or a single one if --name is provided.
36+ */
37+ public function up (ResponseInterface $ response ): ResponseInterface
38+ {
39+ if (!$ this ->confirm ("Are you sure you want to run migration UP? " )) {
40+ return $ response ;
41+ }
42+
43+ $ this ->runDirection ('up ' );
44+
45+ return $ response ;
46+ }
47+
48+ /**
49+ * Run migrations down, or a single one if --name is provided.
50+ */
51+ public function down (ResponseInterface $ response ): ResponseInterface
52+ {
53+ if (!$ this ->confirm ("Are you sure you want to run migration DOWN? " )) {
54+ return $ response ;
55+ }
56+
57+ $ this ->runDirection ('down ' );
58+
59+ return $ response ;
60+ }
61+
62+ /**
63+ * Roll back and re-run migrations (down + up), or a single one if --name is provided.
64+ */
65+ public function fresh (ResponseInterface $ response ): ResponseInterface
66+ {
67+ if (!$ this ->confirm ("Are you sure you want to refresh migration? " )) {
68+ return $ response ;
69+ }
70+
71+ $ this ->runDirection ('down ' );
72+ $ this ->runDirection ('up ' );
73+
74+ return $ response ;
75+ }
76+
77+ // -------------------------------------------------------------------------
78+ // Private helpers
79+ // -------------------------------------------------------------------------
80+
81+ /**
82+ * Run all migration files in the given direction, or only the one
83+ * matching --name if supplied.
84+ */
85+ private function runDirection (string $ direction ): void
86+ {
87+ $ name = !empty ($ this ->args ['name ' ]) ? strtolower ($ this ->args ['name ' ]) : null ;
88+ $ files = $ this ->getMigrationFiles ();
89+
90+ if ($ name !== null ) {
91+ $ files = array_filter ($ files , fn (string $ file ) => strtolower (pathinfo ($ file , PATHINFO_FILENAME )) === $ name );
92+
93+ if (empty ($ files )) {
94+ throw new RuntimeException ("No migration file found matching \"$ name \". " );
95+ }
96+ }
97+
98+ foreach ($ files as $ file ) {
99+ $ this ->runMigration ($ file , $ direction );
100+ }
101+ }
102+
103+ /**
104+ * Instantiate, execute, and persist a single migration file.
105+ */
106+ private function runMigration (string $ file , string $ direction ): void
107+ {
108+ $ base = pathinfo ($ file , PATHINFO_FILENAME );
109+ $ class = "\\Migrations \\" . ucfirst ($ base );
110+
111+ if (!class_exists ($ class )) {
112+ throw new RuntimeException ("Migration class $ class does not exist. " );
113+ }
114+
115+ $ inst = new $ class ();
116+ if (!($ inst instanceof Migrations)) {
117+ throw new RuntimeException ("Migration class must extend: " . Migrations::class);
118+ }
119+
120+ $ schemaManager = $ this ->getSchemaManager ();
121+ $ currentSchema = $ schemaManager ->introspectSchema ();
122+ $ targetSchema = clone $ currentSchema ;
123+
124+ $ inst ->{$ direction }($ targetSchema );
125+
126+ $ this ->executeSchema ($ currentSchema , $ targetSchema );
127+
128+ $ label = strtoupper ($ direction );
129+ $ this ->command ->approve ("Executed migration $ label: $ class " );
130+ }
131+
132+ /**
133+ * Diff two schemas and execute the resulting SQL, if any.
134+ */
135+ private function executeSchema (Schema $ fromSchema , Schema $ toSchema ): void
136+ {
137+ $ connection = DB ::getConnection ();
138+ $ diff = $ this ->getSchemaManager ()->createComparator ()->compareSchemas ($ fromSchema , $ toSchema );
139+ $ statements = $ connection ->getDatabasePlatform ()->getAlterSchemaSQL ($ diff );
140+
141+ if (empty ($ statements )) {
142+ return ;
143+ }
144+
145+ foreach ($ statements as $ sql ) {
146+ if (isset ($ this ->args ['read ' ])) {
147+ $ this ->command ->message ($ sql );
148+ } else {
149+ $ connection ->executeStatement ($ sql );
150+ }
151+ }
152+ }
153+
154+ /**
155+ * Lazy-load and cache the schema manager.
156+ */
157+ private function getSchemaManager (): AbstractSchemaManager
158+ {
159+ if ($ this ->schemaManager === null ) {
160+ $ this ->schemaManager = DB ::getConnection ()->createSchemaManager ();
161+ }
162+
163+ return $ this ->schemaManager ;
164+ }
165+
166+ /**
167+ * Return all migration files, sorted for deterministic order.
168+ */
169+ private function getMigrationFiles (): array
170+ {
171+ $ migDir = App::get ()->dir ()->migrations ();
172+
173+ if (!is_dir ($ migDir )) {
174+ mkdir ($ migDir , 0755 , true );
175+ }
176+
177+ $ files = glob ($ migDir . "/*.php " ) ?: [];
178+ sort ($ files );
179+
180+ return $ files ;
181+ }
182+
183+ /**
184+ * Show a confirmation prompt and abort with a message if declined.
185+ */
186+ private function confirm (string $ question ): bool
187+ {
188+ if ($ this ->command ->confirm ($ question )) {
189+ return true ;
190+ }
191+
192+ $ this ->command ->message ("" );
193+ $ this ->command ->message ($ this ->command ->getAnsi ()->yellow ("Aborting migrations... " ));
194+ $ this ->command ->message ("" );
195+
196+ return false ;
197+ }
198+ }
0 commit comments