Subversion-Projekte lars-tiefland.laravel_shop

Revision

Revision 399 | Zur aktuellen Revision | Details | Vergleich mit vorheriger | Letzte Änderung | Log anzeigen | RSS feed

Revision Autor Zeilennr. Zeile
148 lars 1
<?php
2
 
3
namespace Illuminate\Database\Migrations;
4
 
5
use Doctrine\DBAL\Schema\SchemaException;
6
use Illuminate\Console\View\Components\BulletList;
7
use Illuminate\Console\View\Components\Error;
8
use Illuminate\Console\View\Components\Info;
9
use Illuminate\Console\View\Components\Task;
10
use Illuminate\Console\View\Components\TwoColumnDetail;
11
use Illuminate\Contracts\Events\Dispatcher;
12
use Illuminate\Database\ConnectionResolverInterface as Resolver;
13
use Illuminate\Database\Events\MigrationEnded;
14
use Illuminate\Database\Events\MigrationsEnded;
15
use Illuminate\Database\Events\MigrationsStarted;
16
use Illuminate\Database\Events\MigrationStarted;
17
use Illuminate\Database\Events\NoPendingMigrations;
18
use Illuminate\Filesystem\Filesystem;
19
use Illuminate\Support\Arr;
20
use Illuminate\Support\Collection;
21
use Illuminate\Support\Str;
22
use ReflectionClass;
23
use Symfony\Component\Console\Output\OutputInterface;
24
 
25
class Migrator
26
{
27
    /**
28
     * The event dispatcher instance.
29
     *
30
     * @var \Illuminate\Contracts\Events\Dispatcher
31
     */
32
    protected $events;
33
 
34
    /**
35
     * The migration repository implementation.
36
     *
37
     * @var \Illuminate\Database\Migrations\MigrationRepositoryInterface
38
     */
39
    protected $repository;
40
 
41
    /**
42
     * The filesystem instance.
43
     *
44
     * @var \Illuminate\Filesystem\Filesystem
45
     */
46
    protected $files;
47
 
48
    /**
49
     * The connection resolver instance.
50
     *
51
     * @var \Illuminate\Database\ConnectionResolverInterface
52
     */
53
    protected $resolver;
54
 
55
    /**
56
     * The name of the default connection.
57
     *
58
     * @var string
59
     */
60
    protected $connection;
61
 
62
    /**
63
     * The paths to all of the migration files.
64
     *
65
     * @var array
66
     */
67
    protected $paths = [];
68
 
69
    /**
621 lars 70
     * The paths that have already been required.
71
     *
72
     * @var array<string, \Illuminate\Database\Migrations\Migration|null>
73
     */
74
    protected static $requiredPathCache = [];
75
 
76
    /**
148 lars 77
     * The output interface implementation.
78
     *
79
     * @var \Symfony\Component\Console\Output\OutputInterface
80
     */
81
    protected $output;
82
 
83
    /**
84
     * Create a new migrator instance.
85
     *
86
     * @param  \Illuminate\Database\Migrations\MigrationRepositoryInterface  $repository
87
     * @param  \Illuminate\Database\ConnectionResolverInterface  $resolver
88
     * @param  \Illuminate\Filesystem\Filesystem  $files
89
     * @param  \Illuminate\Contracts\Events\Dispatcher|null  $dispatcher
90
     * @return void
91
     */
92
    public function __construct(MigrationRepositoryInterface $repository,
93
                                Resolver $resolver,
94
                                Filesystem $files,
95
                                Dispatcher $dispatcher = null)
96
    {
97
        $this->files = $files;
98
        $this->events = $dispatcher;
99
        $this->resolver = $resolver;
100
        $this->repository = $repository;
101
    }
102
 
103
    /**
104
     * Run the pending migrations at a given path.
105
     *
106
     * @param  array|string  $paths
107
     * @param  array  $options
108
     * @return array
109
     */
110
    public function run($paths = [], array $options = [])
111
    {
112
        // Once we grab all of the migration files for the path, we will compare them
113
        // against the migrations that have already been run for this package then
114
        // run each of the outstanding migrations against a database connection.
115
        $files = $this->getMigrationFiles($paths);
116
 
117
        $this->requireFiles($migrations = $this->pendingMigrations(
118
            $files, $this->repository->getRan()
119
        ));
120
 
121
        // Once we have all these migrations that are outstanding we are ready to run
122
        // we will go ahead and run them "up". This will execute each migration as
123
        // an operation against a database. Then we'll return this list of them.
124
        $this->runPending($migrations, $options);
125
 
126
        return $migrations;
127
    }
128
 
129
    /**
130
     * Get the migration files that have not yet run.
131
     *
132
     * @param  array  $files
133
     * @param  array  $ran
134
     * @return array
135
     */
136
    protected function pendingMigrations($files, $ran)
137
    {
138
        return Collection::make($files)
139
                ->reject(function ($file) use ($ran) {
140
                    return in_array($this->getMigrationName($file), $ran);
141
                })->values()->all();
142
    }
143
 
144
    /**
145
     * Run an array of migrations.
146
     *
147
     * @param  array  $migrations
148
     * @param  array  $options
149
     * @return void
150
     */
151
    public function runPending(array $migrations, array $options = [])
152
    {
153
        // First we will just make sure that there are any migrations to run. If there
154
        // aren't, we will just make a note of it to the developer so they're aware
155
        // that all of the migrations have been run against this database system.
156
        if (count($migrations) === 0) {
157
            $this->fireMigrationEvent(new NoPendingMigrations('up'));
158
 
159
            $this->write(Info::class, 'Nothing to migrate');
160
 
161
            return;
162
        }
163
 
164
        // Next, we will get the next batch number for the migrations so we can insert
165
        // correct batch number in the database migrations repository when we store
166
        // each migration's execution. We will also extract a few of the options.
167
        $batch = $this->repository->getNextBatchNumber();
168
 
169
        $pretend = $options['pretend'] ?? false;
170
 
171
        $step = $options['step'] ?? false;
172
 
173
        $this->fireMigrationEvent(new MigrationsStarted('up'));
174
 
175
        $this->write(Info::class, 'Running migrations.');
176
 
177
        // Once we have the array of migrations, we will spin through them and run the
178
        // migrations "up" so the changes are made to the databases. We'll then log
179
        // that the migration was run so we don't repeat it next time we execute.
180
        foreach ($migrations as $file) {
181
            $this->runUp($file, $batch, $pretend);
182
 
183
            if ($step) {
184
                $batch++;
185
            }
186
        }
187
 
188
        $this->fireMigrationEvent(new MigrationsEnded('up'));
189
 
190
        if ($this->output) {
191
            $this->output->writeln('');
192
        }
193
    }
194
 
195
    /**
196
     * Run "up" a migration instance.
197
     *
198
     * @param  string  $file
199
     * @param  int  $batch
200
     * @param  bool  $pretend
201
     * @return void
202
     */
203
    protected function runUp($file, $batch, $pretend)
204
    {
205
        // First we will resolve a "real" instance of the migration class from this
206
        // migration file name. Once we have the instances we can run the actual
207
        // command such as "up" or "down", or we can just simulate the action.
208
        $migration = $this->resolvePath($file);
209
 
210
        $name = $this->getMigrationName($file);
211
 
212
        if ($pretend) {
213
            return $this->pretendToRun($migration, 'up');
214
        }
215
 
216
        $this->write(Task::class, $name, fn () => $this->runMigration($migration, 'up'));
217
 
218
        // Once we have run a migrations class, we will log that it was run in this
219
        // repository so that we don't try to run it next time we do a migration
220
        // in the application. A migration repository keeps the migrate order.
221
        $this->repository->log($name, $batch);
222
    }
223
 
224
    /**
225
     * Rollback the last migration operation.
226
     *
227
     * @param  array|string  $paths
228
     * @param  array  $options
229
     * @return array
230
     */
231
    public function rollback($paths = [], array $options = [])
232
    {
233
        // We want to pull in the last batch of migrations that ran on the previous
234
        // migration operation. We'll then reverse those migrations and run each
235
        // of them "down" to reverse the last migration "operation" which ran.
236
        $migrations = $this->getMigrationsForRollback($options);
237
 
238
        if (count($migrations) === 0) {
239
            $this->fireMigrationEvent(new NoPendingMigrations('down'));
240
 
241
            $this->write(Info::class, 'Nothing to rollback.');
242
 
243
            return [];
244
        }
245
 
246
        return tap($this->rollbackMigrations($migrations, $paths, $options), function () {
247
            if ($this->output) {
248
                $this->output->writeln('');
249
            }
250
        });
251
    }
252
 
253
    /**
254
     * Get the migrations for a rollback operation.
255
     *
256
     * @param  array  $options
257
     * @return array
258
     */
259
    protected function getMigrationsForRollback(array $options)
260
    {
261
        if (($steps = $options['step'] ?? 0) > 0) {
262
            return $this->repository->getMigrations($steps);
263
        }
264
 
265
        return $this->repository->getLast();
266
    }
267
 
268
    /**
269
     * Rollback the given migrations.
270
     *
271
     * @param  array  $migrations
272
     * @param  array|string  $paths
273
     * @param  array  $options
274
     * @return array
275
     */
276
    protected function rollbackMigrations(array $migrations, $paths, array $options)
277
    {
278
        $rolledBack = [];
279
 
280
        $this->requireFiles($files = $this->getMigrationFiles($paths));
281
 
282
        $this->fireMigrationEvent(new MigrationsStarted('down'));
283
 
284
        $this->write(Info::class, 'Rolling back migrations.');
285
 
286
        // Next we will run through all of the migrations and call the "down" method
287
        // which will reverse each migration in order. This getLast method on the
288
        // repository already returns these migration's names in reverse order.
289
        foreach ($migrations as $migration) {
290
            $migration = (object) $migration;
291
 
292
            if (! $file = Arr::get($files, $migration->migration)) {
293
                $this->write(TwoColumnDetail::class, $migration->migration, '<fg=yellow;options=bold>Migration not found</>');
294
 
295
                continue;
296
            }
297
 
298
            $rolledBack[] = $file;
299
 
300
            $this->runDown(
301
                $file, $migration,
302
                $options['pretend'] ?? false
303
            );
304
        }
305
 
306
        $this->fireMigrationEvent(new MigrationsEnded('down'));
307
 
308
        return $rolledBack;
309
    }
310
 
311
    /**
312
     * Rolls all of the currently applied migrations back.
313
     *
314
     * @param  array|string  $paths
315
     * @param  bool  $pretend
316
     * @return array
317
     */
318
    public function reset($paths = [], $pretend = false)
319
    {
320
        // Next, we will reverse the migration list so we can run them back in the
321
        // correct order for resetting this database. This will allow us to get
322
        // the database back into its "empty" state ready for the migrations.
323
        $migrations = array_reverse($this->repository->getRan());
324
 
325
        if (count($migrations) === 0) {
326
            $this->write(Info::class, 'Nothing to rollback.');
327
 
328
            return [];
329
        }
330
 
331
        return tap($this->resetMigrations($migrations, $paths, $pretend), function () {
332
            if ($this->output) {
333
                $this->output->writeln('');
334
            }
335
        });
336
    }
337
 
338
    /**
339
     * Reset the given migrations.
340
     *
341
     * @param  array  $migrations
342
     * @param  array  $paths
343
     * @param  bool  $pretend
344
     * @return array
345
     */
346
    protected function resetMigrations(array $migrations, array $paths, $pretend = false)
347
    {
348
        // Since the getRan method that retrieves the migration name just gives us the
349
        // migration name, we will format the names into objects with the name as a
350
        // property on the objects so that we can pass it to the rollback method.
351
        $migrations = collect($migrations)->map(function ($m) {
352
            return (object) ['migration' => $m];
353
        })->all();
354
 
355
        return $this->rollbackMigrations(
356
            $migrations, $paths, compact('pretend')
357
        );
358
    }
359
 
360
    /**
361
     * Run "down" a migration instance.
362
     *
363
     * @param  string  $file
364
     * @param  object  $migration
365
     * @param  bool  $pretend
366
     * @return void
367
     */
368
    protected function runDown($file, $migration, $pretend)
369
    {
370
        // First we will get the file name of the migration so we can resolve out an
371
        // instance of the migration. Once we get an instance we can either run a
372
        // pretend execution of the migration or we can run the real migration.
373
        $instance = $this->resolvePath($file);
374
 
375
        $name = $this->getMigrationName($file);
376
 
377
        if ($pretend) {
378
            return $this->pretendToRun($instance, 'down');
379
        }
380
 
381
        $this->write(Task::class, $name, fn () => $this->runMigration($instance, 'down'));
382
 
383
        // Once we have successfully run the migration "down" we will remove it from
384
        // the migration repository so it will be considered to have not been run
385
        // by the application then will be able to fire by any later operation.
386
        $this->repository->delete($migration);
387
    }
388
 
389
    /**
390
     * Run a migration inside a transaction if the database supports it.
391
     *
392
     * @param  object  $migration
393
     * @param  string  $method
394
     * @return void
395
     */
396
    protected function runMigration($migration, $method)
397
    {
398
        $connection = $this->resolveConnection(
399
            $migration->getConnection()
400
        );
401
 
402
        $callback = function () use ($connection, $migration, $method) {
403
            if (method_exists($migration, $method)) {
404
                $this->fireMigrationEvent(new MigrationStarted($migration, $method));
405
 
406
                $this->runMethod($connection, $migration, $method);
407
 
408
                $this->fireMigrationEvent(new MigrationEnded($migration, $method));
409
            }
410
        };
411
 
412
        $this->getSchemaGrammar($connection)->supportsSchemaTransactions()
413
            && $migration->withinTransaction
414
                    ? $connection->transaction($callback)
415
                    : $callback();
416
    }
417
 
418
    /**
419
     * Pretend to run the migrations.
420
     *
421
     * @param  object  $migration
422
     * @param  string  $method
423
     * @return void
424
     */
425
    protected function pretendToRun($migration, $method)
426
    {
427
        try {
428
            $name = get_class($migration);
429
 
430
            $reflectionClass = new ReflectionClass($migration);
431
 
432
            if ($reflectionClass->isAnonymous()) {
433
                $name = $this->getMigrationName($reflectionClass->getFileName());
434
            }
435
 
436
            $this->write(TwoColumnDetail::class, $name);
437
            $this->write(BulletList::class, collect($this->getQueries($migration, $method))->map(function ($query) {
438
                return $query['query'];
439
            }));
440
        } catch (SchemaException $e) {
441
            $name = get_class($migration);
442
 
443
            $this->write(Error::class, sprintf(
444
                '[%s] failed to dump queries. This may be due to changing database columns using Doctrine, which is not supported while pretending to run migrations.',
445
                $name,
446
            ));
447
        }
448
    }
449
 
450
    /**
451
     * Get all of the queries that would be run for a migration.
452
     *
453
     * @param  object  $migration
454
     * @param  string  $method
455
     * @return array
456
     */
457
    protected function getQueries($migration, $method)
458
    {
459
        // Now that we have the connections we can resolve it and pretend to run the
460
        // queries against the database returning the array of raw SQL statements
461
        // that would get fired against the database system for this migration.
462
        $db = $this->resolveConnection(
463
            $migration->getConnection()
464
        );
465
 
466
        return $db->pretend(function () use ($db, $migration, $method) {
467
            if (method_exists($migration, $method)) {
468
                $this->runMethod($db, $migration, $method);
469
            }
470
        });
471
    }
472
 
473
    /**
474
     * Run a migration method on the given connection.
475
     *
476
     * @param  \Illuminate\Database\Connection  $connection
477
     * @param  object  $migration
478
     * @param  string  $method
479
     * @return void
480
     */
481
    protected function runMethod($connection, $migration, $method)
482
    {
483
        $previousConnection = $this->resolver->getDefaultConnection();
484
 
485
        try {
486
            $this->resolver->setDefaultConnection($connection->getName());
487
 
488
            $migration->{$method}();
489
        } finally {
490
            $this->resolver->setDefaultConnection($previousConnection);
491
        }
492
    }
493
 
494
    /**
495
     * Resolve a migration instance from a file.
496
     *
497
     * @param  string  $file
498
     * @return object
499
     */
500
    public function resolve($file)
501
    {
502
        $class = $this->getMigrationClass($file);
503
 
504
        return new $class;
505
    }
506
 
507
    /**
508
     * Resolve a migration instance from a migration path.
509
     *
510
     * @param  string  $path
511
     * @return object
512
     */
513
    protected function resolvePath(string $path)
514
    {
515
        $class = $this->getMigrationClass($this->getMigrationName($path));
516
 
517
        if (class_exists($class) && realpath($path) == (new ReflectionClass($class))->getFileName()) {
518
            return new $class;
519
        }
520
 
621 lars 521
        $migration = static::$requiredPathCache[$path] ??= $this->files->getRequire($path);
148 lars 522
 
621 lars 523
        if (is_object($migration)) {
524
            return clone $migration;
525
        }
526
 
527
        return new $class;
148 lars 528
    }
529
 
530
    /**
531
     * Generate a migration class name based on the migration file name.
532
     *
533
     * @param  string  $migrationName
534
     * @return string
535
     */
536
    protected function getMigrationClass(string $migrationName): string
537
    {
538
        return Str::studly(implode('_', array_slice(explode('_', $migrationName), 4)));
539
    }
540
 
541
    /**
542
     * Get all of the migration files in a given path.
543
     *
544
     * @param  string|array  $paths
545
     * @return array
546
     */
547
    public function getMigrationFiles($paths)
548
    {
549
        return Collection::make($paths)->flatMap(function ($path) {
550
            return str_ends_with($path, '.php') ? [$path] : $this->files->glob($path.'/*_*.php');
551
        })->filter()->values()->keyBy(function ($file) {
552
            return $this->getMigrationName($file);
553
        })->sortBy(function ($file, $key) {
554
            return $key;
555
        })->all();
556
    }
557
 
558
    /**
559
     * Require in all the migration files in a given path.
560
     *
561
     * @param  array  $files
562
     * @return void
563
     */
564
    public function requireFiles(array $files)
565
    {
566
        foreach ($files as $file) {
567
            $this->files->requireOnce($file);
568
        }
569
    }
570
 
571
    /**
572
     * Get the name of the migration.
573
     *
574
     * @param  string  $path
575
     * @return string
576
     */
577
    public function getMigrationName($path)
578
    {
579
        return str_replace('.php', '', basename($path));
580
    }
581
 
582
    /**
583
     * Register a custom migration path.
584
     *
585
     * @param  string  $path
586
     * @return void
587
     */
588
    public function path($path)
589
    {
590
        $this->paths = array_unique(array_merge($this->paths, [$path]));
591
    }
592
 
593
    /**
594
     * Get all of the custom migration paths.
595
     *
596
     * @return array
597
     */
598
    public function paths()
599
    {
600
        return $this->paths;
601
    }
602
 
603
    /**
604
     * Get the default connection name.
605
     *
606
     * @return string
607
     */
608
    public function getConnection()
609
    {
610
        return $this->connection;
611
    }
612
 
613
    /**
614
     * Execute the given callback using the given connection as the default connection.
615
     *
616
     * @param  string  $name
617
     * @param  callable  $callback
618
     * @return mixed
619
     */
620
    public function usingConnection($name, callable $callback)
621
    {
622
        $previousConnection = $this->resolver->getDefaultConnection();
623
 
624
        $this->setConnection($name);
625
 
626
        return tap($callback(), function () use ($previousConnection) {
627
            $this->setConnection($previousConnection);
628
        });
629
    }
630
 
631
    /**
632
     * Set the default connection name.
633
     *
634
     * @param  string  $name
635
     * @return void
636
     */
637
    public function setConnection($name)
638
    {
639
        if (! is_null($name)) {
640
            $this->resolver->setDefaultConnection($name);
641
        }
642
 
643
        $this->repository->setSource($name);
644
 
645
        $this->connection = $name;
646
    }
647
 
648
    /**
649
     * Resolve the database connection instance.
650
     *
651
     * @param  string  $connection
652
     * @return \Illuminate\Database\Connection
653
     */
654
    public function resolveConnection($connection)
655
    {
656
        return $this->resolver->connection($connection ?: $this->connection);
657
    }
658
 
659
    /**
660
     * Get the schema grammar out of a migration connection.
661
     *
662
     * @param  \Illuminate\Database\Connection  $connection
663
     * @return \Illuminate\Database\Schema\Grammars\Grammar
664
     */
665
    protected function getSchemaGrammar($connection)
666
    {
667
        if (is_null($grammar = $connection->getSchemaGrammar())) {
668
            $connection->useDefaultSchemaGrammar();
669
 
670
            $grammar = $connection->getSchemaGrammar();
671
        }
672
 
673
        return $grammar;
674
    }
675
 
676
    /**
677
     * Get the migration repository instance.
678
     *
679
     * @return \Illuminate\Database\Migrations\MigrationRepositoryInterface
680
     */
681
    public function getRepository()
682
    {
683
        return $this->repository;
684
    }
685
 
686
    /**
687
     * Determine if the migration repository exists.
688
     *
689
     * @return bool
690
     */
691
    public function repositoryExists()
692
    {
693
        return $this->repository->repositoryExists();
694
    }
695
 
696
    /**
697
     * Determine if any migrations have been run.
698
     *
699
     * @return bool
700
     */
701
    public function hasRunAnyMigrations()
702
    {
703
        return $this->repositoryExists() && count($this->repository->getRan()) > 0;
704
    }
705
 
706
    /**
707
     * Delete the migration repository data store.
708
     *
709
     * @return void
710
     */
711
    public function deleteRepository()
712
    {
713
        return $this->repository->deleteRepository();
714
    }
715
 
716
    /**
717
     * Get the file system instance.
718
     *
719
     * @return \Illuminate\Filesystem\Filesystem
720
     */
721
    public function getFilesystem()
722
    {
723
        return $this->files;
724
    }
725
 
726
    /**
727
     * Set the output implementation that should be used by the console.
728
     *
729
     * @param  \Symfony\Component\Console\Output\OutputInterface  $output
730
     * @return $this
731
     */
732
    public function setOutput(OutputInterface $output)
733
    {
734
        $this->output = $output;
735
 
736
        return $this;
737
    }
738
 
739
    /**
740
     * Write to the console's output.
741
     *
742
     * @param  string  $component
399 lars 743
     * @param  array<int, string>|string  ...$arguments
148 lars 744
     * @return void
745
     */
746
    protected function write($component, ...$arguments)
747
    {
748
        if ($this->output && class_exists($component)) {
749
            (new $component($this->output))->render(...$arguments);
750
        } else {
751
            foreach ($arguments as $argument) {
752
                if (is_callable($argument)) {
753
                    $argument();
754
                }
755
            }
756
        }
757
    }
758
 
759
    /**
760
     * Fire the given event for the migration.
761
     *
762
     * @param  \Illuminate\Contracts\Database\Events\MigrationEvent  $event
763
     * @return void
764
     */
765
    public function fireMigrationEvent($event)
766
    {
767
        if ($this->events) {
768
            $this->events->dispatch($event);
769
        }
770
    }
771
}