Subversion-Projekte lars-tiefland.laravel_shop

Revision

Revision 621 | 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)) {
688 lars 524
            return method_exists($migration, '__construct')
525
                    ? $this->files->getRequire($path)
526
                    : clone $migration;
621 lars 527
        }
528
 
529
        return new $class;
148 lars 530
    }
531
 
532
    /**
533
     * Generate a migration class name based on the migration file name.
534
     *
535
     * @param  string  $migrationName
536
     * @return string
537
     */
538
    protected function getMigrationClass(string $migrationName): string
539
    {
540
        return Str::studly(implode('_', array_slice(explode('_', $migrationName), 4)));
541
    }
542
 
543
    /**
544
     * Get all of the migration files in a given path.
545
     *
546
     * @param  string|array  $paths
547
     * @return array
548
     */
549
    public function getMigrationFiles($paths)
550
    {
551
        return Collection::make($paths)->flatMap(function ($path) {
552
            return str_ends_with($path, '.php') ? [$path] : $this->files->glob($path.'/*_*.php');
553
        })->filter()->values()->keyBy(function ($file) {
554
            return $this->getMigrationName($file);
555
        })->sortBy(function ($file, $key) {
556
            return $key;
557
        })->all();
558
    }
559
 
560
    /**
561
     * Require in all the migration files in a given path.
562
     *
563
     * @param  array  $files
564
     * @return void
565
     */
566
    public function requireFiles(array $files)
567
    {
568
        foreach ($files as $file) {
569
            $this->files->requireOnce($file);
570
        }
571
    }
572
 
573
    /**
574
     * Get the name of the migration.
575
     *
576
     * @param  string  $path
577
     * @return string
578
     */
579
    public function getMigrationName($path)
580
    {
581
        return str_replace('.php', '', basename($path));
582
    }
583
 
584
    /**
585
     * Register a custom migration path.
586
     *
587
     * @param  string  $path
588
     * @return void
589
     */
590
    public function path($path)
591
    {
592
        $this->paths = array_unique(array_merge($this->paths, [$path]));
593
    }
594
 
595
    /**
596
     * Get all of the custom migration paths.
597
     *
598
     * @return array
599
     */
600
    public function paths()
601
    {
602
        return $this->paths;
603
    }
604
 
605
    /**
606
     * Get the default connection name.
607
     *
608
     * @return string
609
     */
610
    public function getConnection()
611
    {
612
        return $this->connection;
613
    }
614
 
615
    /**
616
     * Execute the given callback using the given connection as the default connection.
617
     *
618
     * @param  string  $name
619
     * @param  callable  $callback
620
     * @return mixed
621
     */
622
    public function usingConnection($name, callable $callback)
623
    {
624
        $previousConnection = $this->resolver->getDefaultConnection();
625
 
626
        $this->setConnection($name);
627
 
628
        return tap($callback(), function () use ($previousConnection) {
629
            $this->setConnection($previousConnection);
630
        });
631
    }
632
 
633
    /**
634
     * Set the default connection name.
635
     *
636
     * @param  string  $name
637
     * @return void
638
     */
639
    public function setConnection($name)
640
    {
641
        if (! is_null($name)) {
642
            $this->resolver->setDefaultConnection($name);
643
        }
644
 
645
        $this->repository->setSource($name);
646
 
647
        $this->connection = $name;
648
    }
649
 
650
    /**
651
     * Resolve the database connection instance.
652
     *
653
     * @param  string  $connection
654
     * @return \Illuminate\Database\Connection
655
     */
656
    public function resolveConnection($connection)
657
    {
658
        return $this->resolver->connection($connection ?: $this->connection);
659
    }
660
 
661
    /**
662
     * Get the schema grammar out of a migration connection.
663
     *
664
     * @param  \Illuminate\Database\Connection  $connection
665
     * @return \Illuminate\Database\Schema\Grammars\Grammar
666
     */
667
    protected function getSchemaGrammar($connection)
668
    {
669
        if (is_null($grammar = $connection->getSchemaGrammar())) {
670
            $connection->useDefaultSchemaGrammar();
671
 
672
            $grammar = $connection->getSchemaGrammar();
673
        }
674
 
675
        return $grammar;
676
    }
677
 
678
    /**
679
     * Get the migration repository instance.
680
     *
681
     * @return \Illuminate\Database\Migrations\MigrationRepositoryInterface
682
     */
683
    public function getRepository()
684
    {
685
        return $this->repository;
686
    }
687
 
688
    /**
689
     * Determine if the migration repository exists.
690
     *
691
     * @return bool
692
     */
693
    public function repositoryExists()
694
    {
695
        return $this->repository->repositoryExists();
696
    }
697
 
698
    /**
699
     * Determine if any migrations have been run.
700
     *
701
     * @return bool
702
     */
703
    public function hasRunAnyMigrations()
704
    {
705
        return $this->repositoryExists() && count($this->repository->getRan()) > 0;
706
    }
707
 
708
    /**
709
     * Delete the migration repository data store.
710
     *
711
     * @return void
712
     */
713
    public function deleteRepository()
714
    {
715
        return $this->repository->deleteRepository();
716
    }
717
 
718
    /**
719
     * Get the file system instance.
720
     *
721
     * @return \Illuminate\Filesystem\Filesystem
722
     */
723
    public function getFilesystem()
724
    {
725
        return $this->files;
726
    }
727
 
728
    /**
729
     * Set the output implementation that should be used by the console.
730
     *
731
     * @param  \Symfony\Component\Console\Output\OutputInterface  $output
732
     * @return $this
733
     */
734
    public function setOutput(OutputInterface $output)
735
    {
736
        $this->output = $output;
737
 
738
        return $this;
739
    }
740
 
741
    /**
742
     * Write to the console's output.
743
     *
744
     * @param  string  $component
399 lars 745
     * @param  array<int, string>|string  ...$arguments
148 lars 746
     * @return void
747
     */
748
    protected function write($component, ...$arguments)
749
    {
750
        if ($this->output && class_exists($component)) {
751
            (new $component($this->output))->render(...$arguments);
752
        } else {
753
            foreach ($arguments as $argument) {
754
                if (is_callable($argument)) {
755
                    $argument();
756
                }
757
            }
758
        }
759
    }
760
 
761
    /**
762
     * Fire the given event for the migration.
763
     *
764
     * @param  \Illuminate\Contracts\Database\Events\MigrationEvent  $event
765
     * @return void
766
     */
767
    public function fireMigrationEvent($event)
768
    {
769
        if ($this->events) {
770
            $this->events->dispatch($event);
771
        }
772
    }
773
}