Subversion-Projekte lars-tiefland.laravel_shop

Revision

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