2021-08-27 06:46:27 -04:00
< ? php
namespace Illuminate\Database\Migrations ;
use Doctrine\DBAL\Schema\SchemaException ;
2023-02-24 06:26:40 -05:00
use Illuminate\Console\View\Components\BulletList ;
use Illuminate\Console\View\Components\Error ;
use Illuminate\Console\View\Components\Info ;
use Illuminate\Console\View\Components\Task ;
use Illuminate\Console\View\Components\TwoColumnDetail ;
2021-08-27 06:46:27 -04:00
use Illuminate\Contracts\Events\Dispatcher ;
use Illuminate\Database\ConnectionResolverInterface as Resolver ;
use Illuminate\Database\Events\MigrationEnded ;
use Illuminate\Database\Events\MigrationsEnded ;
use Illuminate\Database\Events\MigrationsStarted ;
use Illuminate\Database\Events\MigrationStarted ;
use Illuminate\Database\Events\NoPendingMigrations ;
use Illuminate\Filesystem\Filesystem ;
use Illuminate\Support\Arr ;
use Illuminate\Support\Collection ;
use Illuminate\Support\Str ;
use ReflectionClass ;
use Symfony\Component\Console\Output\OutputInterface ;
class Migrator
{
/**
* The event dispatcher instance .
*
* @ var \Illuminate\Contracts\Events\Dispatcher
*/
protected $events ;
/**
* The migration repository implementation .
*
* @ var \Illuminate\Database\Migrations\MigrationRepositoryInterface
*/
protected $repository ;
/**
* The filesystem instance .
*
* @ var \Illuminate\Filesystem\Filesystem
*/
protected $files ;
/**
* The connection resolver instance .
*
* @ var \Illuminate\Database\ConnectionResolverInterface
*/
protected $resolver ;
/**
* The name of the default connection .
*
* @ var string
*/
protected $connection ;
/**
* The paths to all of the migration files .
*
* @ var array
*/
protected $paths = [];
2023-02-24 06:26:40 -05:00
/**
* The paths that have already been required .
*
* @ var array < string , \Illuminate\Database\Migrations\Migration | null >
*/
protected static $requiredPathCache = [];
2021-08-27 06:46:27 -04:00
/**
* The output interface implementation .
*
* @ var \Symfony\Component\Console\Output\OutputInterface
*/
protected $output ;
/**
* Create a new migrator instance .
*
* @ param \Illuminate\Database\Migrations\MigrationRepositoryInterface $repository
* @ param \Illuminate\Database\ConnectionResolverInterface $resolver
* @ param \Illuminate\Filesystem\Filesystem $files
* @ param \Illuminate\Contracts\Events\Dispatcher | null $dispatcher
* @ return void
*/
public function __construct ( MigrationRepositoryInterface $repository ,
Resolver $resolver ,
Filesystem $files ,
Dispatcher $dispatcher = null )
{
$this -> files = $files ;
$this -> events = $dispatcher ;
$this -> resolver = $resolver ;
$this -> repository = $repository ;
}
/**
* Run the pending migrations at a given path .
*
* @ param array | string $paths
* @ param array $options
* @ return array
*/
public function run ( $paths = [], array $options = [])
{
// Once we grab all of the migration files for the path, we will compare them
// against the migrations that have already been run for this package then
// run each of the outstanding migrations against a database connection.
$files = $this -> getMigrationFiles ( $paths );
$this -> requireFiles ( $migrations = $this -> pendingMigrations (
$files , $this -> repository -> getRan ()
));
// Once we have all these migrations that are outstanding we are ready to run
// we will go ahead and run them "up". This will execute each migration as
// an operation against a database. Then we'll return this list of them.
$this -> runPending ( $migrations , $options );
return $migrations ;
}
/**
* Get the migration files that have not yet run .
*
* @ param array $files
* @ param array $ran
* @ return array
*/
protected function pendingMigrations ( $files , $ran )
{
return Collection :: make ( $files )
-> reject ( function ( $file ) use ( $ran ) {
return in_array ( $this -> getMigrationName ( $file ), $ran );
}) -> values () -> all ();
}
/**
* Run an array of migrations .
*
* @ param array $migrations
* @ param array $options
* @ return void
*/
public function runPending ( array $migrations , array $options = [])
{
// First we will just make sure that there are any migrations to run. If there
// aren't, we will just make a note of it to the developer so they're aware
// that all of the migrations have been run against this database system.
if ( count ( $migrations ) === 0 ) {
$this -> fireMigrationEvent ( new NoPendingMigrations ( 'up' ));
2023-02-24 06:26:40 -05:00
$this -> write ( Info :: class , 'Nothing to migrate' );
2021-08-27 06:46:27 -04:00
return ;
}
// Next, we will get the next batch number for the migrations so we can insert
// correct batch number in the database migrations repository when we store
// each migration's execution. We will also extract a few of the options.
$batch = $this -> repository -> getNextBatchNumber ();
$pretend = $options [ 'pretend' ] ? ? false ;
$step = $options [ 'step' ] ? ? false ;
2022-03-14 16:22:30 -04:00
$this -> fireMigrationEvent ( new MigrationsStarted ( 'up' ));
2021-08-27 06:46:27 -04:00
2023-02-24 06:26:40 -05:00
$this -> write ( Info :: class , 'Running migrations.' );
2021-08-27 06:46:27 -04:00
// Once we have the array of migrations, we will spin through them and run the
// migrations "up" so the changes are made to the databases. We'll then log
// that the migration was run so we don't repeat it next time we execute.
foreach ( $migrations as $file ) {
$this -> runUp ( $file , $batch , $pretend );
if ( $step ) {
$batch ++ ;
}
}
2022-03-14 16:22:30 -04:00
$this -> fireMigrationEvent ( new MigrationsEnded ( 'up' ));
2023-02-24 06:26:40 -05:00
if ( $this -> output ) {
$this -> output -> writeln ( '' );
}
2021-08-27 06:46:27 -04:00
}
/**
* Run " up " a migration instance .
*
* @ param string $file
* @ param int $batch
* @ param bool $pretend
* @ return void
*/
protected function runUp ( $file , $batch , $pretend )
{
// First we will resolve a "real" instance of the migration class from this
// migration file name. Once we have the instances we can run the actual
// command such as "up" or "down", or we can just simulate the action.
$migration = $this -> resolvePath ( $file );
$name = $this -> getMigrationName ( $file );
if ( $pretend ) {
return $this -> pretendToRun ( $migration , 'up' );
}
2023-02-24 06:26:40 -05:00
$this -> write ( Task :: class , $name , fn () => $this -> runMigration ( $migration , 'up' ));
2021-08-27 06:46:27 -04:00
// Once we have run a migrations class, we will log that it was run in this
// repository so that we don't try to run it next time we do a migration
// in the application. A migration repository keeps the migrate order.
$this -> repository -> log ( $name , $batch );
}
/**
* Rollback the last migration operation .
*
* @ param array | string $paths
* @ param array $options
* @ return array
*/
public function rollback ( $paths = [], array $options = [])
{
// We want to pull in the last batch of migrations that ran on the previous
// migration operation. We'll then reverse those migrations and run each
// of them "down" to reverse the last migration "operation" which ran.
$migrations = $this -> getMigrationsForRollback ( $options );
if ( count ( $migrations ) === 0 ) {
$this -> fireMigrationEvent ( new NoPendingMigrations ( 'down' ));
2023-02-24 06:26:40 -05:00
$this -> write ( Info :: class , 'Nothing to rollback.' );
2021-08-27 06:46:27 -04:00
return [];
}
2023-02-24 06:26:40 -05:00
return tap ( $this -> rollbackMigrations ( $migrations , $paths , $options ), function () {
if ( $this -> output ) {
$this -> output -> writeln ( '' );
}
});
2021-08-27 06:46:27 -04:00
}
/**
* Get the migrations for a rollback operation .
*
* @ param array $options
* @ return array
*/
protected function getMigrationsForRollback ( array $options )
{
if (( $steps = $options [ 'step' ] ? ? 0 ) > 0 ) {
return $this -> repository -> getMigrations ( $steps );
}
return $this -> repository -> getLast ();
}
/**
* Rollback the given migrations .
*
* @ param array $migrations
* @ param array | string $paths
* @ param array $options
* @ return array
*/
protected function rollbackMigrations ( array $migrations , $paths , array $options )
{
$rolledBack = [];
$this -> requireFiles ( $files = $this -> getMigrationFiles ( $paths ));
2022-03-14 16:22:30 -04:00
$this -> fireMigrationEvent ( new MigrationsStarted ( 'down' ));
2021-08-27 06:46:27 -04:00
2023-02-24 06:26:40 -05:00
$this -> write ( Info :: class , 'Rolling back migrations.' );
2021-08-27 06:46:27 -04:00
// Next we will run through all of the migrations and call the "down" method
// which will reverse each migration in order. This getLast method on the
// repository already returns these migration's names in reverse order.
foreach ( $migrations as $migration ) {
$migration = ( object ) $migration ;
if ( ! $file = Arr :: get ( $files , $migration -> migration )) {
2023-02-24 06:26:40 -05:00
$this -> write ( TwoColumnDetail :: class , $migration -> migration , '<fg=yellow;options=bold>Migration not found</>' );
2021-08-27 06:46:27 -04:00
continue ;
}
$rolledBack [] = $file ;
$this -> runDown (
$file , $migration ,
$options [ 'pretend' ] ? ? false
);
}
2022-03-14 16:22:30 -04:00
$this -> fireMigrationEvent ( new MigrationsEnded ( 'down' ));
2021-08-27 06:46:27 -04:00
return $rolledBack ;
}
/**
* Rolls all of the currently applied migrations back .
*
* @ param array | string $paths
* @ param bool $pretend
* @ return array
*/
public function reset ( $paths = [], $pretend = false )
{
// Next, we will reverse the migration list so we can run them back in the
// correct order for resetting this database. This will allow us to get
// the database back into its "empty" state ready for the migrations.
$migrations = array_reverse ( $this -> repository -> getRan ());
if ( count ( $migrations ) === 0 ) {
2023-02-24 06:26:40 -05:00
$this -> write ( Info :: class , 'Nothing to rollback.' );
2021-08-27 06:46:27 -04:00
return [];
}
2023-02-24 06:26:40 -05:00
return tap ( $this -> resetMigrations ( $migrations , $paths , $pretend ), function () {
if ( $this -> output ) {
$this -> output -> writeln ( '' );
}
});
2021-08-27 06:46:27 -04:00
}
/**
* Reset the given migrations .
*
* @ param array $migrations
* @ param array $paths
* @ param bool $pretend
* @ return array
*/
protected function resetMigrations ( array $migrations , array $paths , $pretend = false )
{
// Since the getRan method that retrieves the migration name just gives us the
// migration name, we will format the names into objects with the name as a
// property on the objects so that we can pass it to the rollback method.
$migrations = collect ( $migrations ) -> map ( function ( $m ) {
return ( object ) [ 'migration' => $m ];
}) -> all ();
return $this -> rollbackMigrations (
$migrations , $paths , compact ( 'pretend' )
);
}
/**
* Run " down " a migration instance .
*
* @ param string $file
* @ param object $migration
* @ param bool $pretend
* @ return void
*/
protected function runDown ( $file , $migration , $pretend )
{
// First we will get the file name of the migration so we can resolve out an
// instance of the migration. Once we get an instance we can either run a
// pretend execution of the migration or we can run the real migration.
$instance = $this -> resolvePath ( $file );
$name = $this -> getMigrationName ( $file );
if ( $pretend ) {
return $this -> pretendToRun ( $instance , 'down' );
}
2023-02-24 06:26:40 -05:00
$this -> write ( Task :: class , $name , fn () => $this -> runMigration ( $instance , 'down' ));
2021-08-27 06:46:27 -04:00
// Once we have successfully run the migration "down" we will remove it from
// the migration repository so it will be considered to have not been run
// by the application then will be able to fire by any later operation.
$this -> repository -> delete ( $migration );
}
/**
* Run a migration inside a transaction if the database supports it .
*
* @ param object $migration
* @ param string $method
* @ return void
*/
protected function runMigration ( $migration , $method )
{
$connection = $this -> resolveConnection (
$migration -> getConnection ()
);
2022-03-14 16:22:30 -04:00
$callback = function () use ( $connection , $migration , $method ) {
2021-08-27 06:46:27 -04:00
if ( method_exists ( $migration , $method )) {
$this -> fireMigrationEvent ( new MigrationStarted ( $migration , $method ));
2022-03-14 16:22:30 -04:00
$this -> runMethod ( $connection , $migration , $method );
2021-08-27 06:46:27 -04:00
$this -> fireMigrationEvent ( new MigrationEnded ( $migration , $method ));
}
};
$this -> getSchemaGrammar ( $connection ) -> supportsSchemaTransactions ()
&& $migration -> withinTransaction
? $connection -> transaction ( $callback )
: $callback ();
}
/**
* Pretend to run the migrations .
*
* @ param object $migration
* @ param string $method
* @ return void
*/
protected function pretendToRun ( $migration , $method )
{
try {
2023-02-24 06:26:40 -05:00
$name = get_class ( $migration );
2021-08-27 06:46:27 -04:00
2023-02-24 06:26:40 -05:00
$reflectionClass = new ReflectionClass ( $migration );
2021-08-27 06:46:27 -04:00
2023-02-24 06:26:40 -05:00
if ( $reflectionClass -> isAnonymous ()) {
$name = $this -> getMigrationName ( $reflectionClass -> getFileName ());
2021-08-27 06:46:27 -04:00
}
2023-02-24 06:26:40 -05:00
$this -> write ( TwoColumnDetail :: class , $name );
$this -> write ( BulletList :: class , collect ( $this -> getQueries ( $migration , $method )) -> map ( function ( $query ) {
return $query [ 'query' ];
}));
2021-08-27 06:46:27 -04:00
} catch ( SchemaException $e ) {
$name = get_class ( $migration );
2023-02-24 06:26:40 -05:00
$this -> write ( Error :: class , sprintf (
'[%s] failed to dump queries. This may be due to changing database columns using Doctrine, which is not supported while pretending to run migrations.' ,
$name ,
));
2021-08-27 06:46:27 -04:00
}
}
/**
* Get all of the queries that would be run for a migration .
*
* @ param object $migration
* @ param string $method
* @ return array
*/
protected function getQueries ( $migration , $method )
{
// Now that we have the connections we can resolve it and pretend to run the
// queries against the database returning the array of raw SQL statements
// that would get fired against the database system for this migration.
$db = $this -> resolveConnection (
$migration -> getConnection ()
);
2022-03-14 16:22:30 -04:00
return $db -> pretend ( function () use ( $db , $migration , $method ) {
2021-08-27 06:46:27 -04:00
if ( method_exists ( $migration , $method )) {
2022-03-14 16:22:30 -04:00
$this -> runMethod ( $db , $migration , $method );
2021-08-27 06:46:27 -04:00
}
});
}
2022-03-14 16:22:30 -04:00
/**
* Run a migration method on the given connection .
*
* @ param \Illuminate\Database\Connection $connection
* @ param object $migration
* @ param string $method
* @ return void
*/
protected function runMethod ( $connection , $migration , $method )
{
$previousConnection = $this -> resolver -> getDefaultConnection ();
try {
$this -> resolver -> setDefaultConnection ( $connection -> getName ());
$migration -> { $method }();
} finally {
$this -> resolver -> setDefaultConnection ( $previousConnection );
}
}
2021-08-27 06:46:27 -04:00
/**
* Resolve a migration instance from a file .
*
* @ param string $file
* @ return object
*/
public function resolve ( $file )
{
$class = $this -> getMigrationClass ( $file );
return new $class ;
}
/**
* Resolve a migration instance from a migration path .
*
* @ param string $path
* @ return object
*/
protected function resolvePath ( string $path )
{
$class = $this -> getMigrationClass ( $this -> getMigrationName ( $path ));
if ( class_exists ( $class ) && realpath ( $path ) == ( new ReflectionClass ( $class )) -> getFileName ()) {
return new $class ;
}
2023-02-24 06:26:40 -05:00
$migration = static :: $requiredPathCache [ $path ] ? ? = $this -> files -> getRequire ( $path );
if ( is_object ( $migration )) {
2023-05-29 11:13:32 -04:00
return method_exists ( $migration , '__construct' )
? $this -> files -> getRequire ( $path )
: clone $migration ;
2023-02-24 06:26:40 -05:00
}
2021-08-27 06:46:27 -04:00
2023-02-24 06:26:40 -05:00
return new $class ;
2021-08-27 06:46:27 -04:00
}
/**
* Generate a migration class name based on the migration file name .
*
* @ param string $migrationName
* @ return string
*/
protected function getMigrationClass ( string $migrationName ) : string
{
return Str :: studly ( implode ( '_' , array_slice ( explode ( '_' , $migrationName ), 4 )));
}
/**
* Get all of the migration files in a given path .
*
* @ param string | array $paths
* @ return array
*/
public function getMigrationFiles ( $paths )
{
return Collection :: make ( $paths ) -> flatMap ( function ( $path ) {
2022-03-14 16:22:30 -04:00
return str_ends_with ( $path , '.php' ) ? [ $path ] : $this -> files -> glob ( $path . '/*_*.php' );
2021-08-27 06:46:27 -04:00
}) -> filter () -> values () -> keyBy ( function ( $file ) {
return $this -> getMigrationName ( $file );
}) -> sortBy ( function ( $file , $key ) {
return $key ;
}) -> all ();
}
/**
* Require in all the migration files in a given path .
*
* @ param array $files
* @ return void
*/
public function requireFiles ( array $files )
{
foreach ( $files as $file ) {
$this -> files -> requireOnce ( $file );
}
}
/**
* Get the name of the migration .
*
* @ param string $path
* @ return string
*/
public function getMigrationName ( $path )
{
return str_replace ( '.php' , '' , basename ( $path ));
}
/**
* Register a custom migration path .
*
* @ param string $path
* @ return void
*/
public function path ( $path )
{
$this -> paths = array_unique ( array_merge ( $this -> paths , [ $path ]));
}
/**
* Get all of the custom migration paths .
*
* @ return array
*/
public function paths ()
{
return $this -> paths ;
}
/**
* Get the default connection name .
*
* @ return string
*/
public function getConnection ()
{
return $this -> connection ;
}
/**
* Execute the given callback using the given connection as the default connection .
*
* @ param string $name
* @ param callable $callback
* @ return mixed
*/
public function usingConnection ( $name , callable $callback )
{
$previousConnection = $this -> resolver -> getDefaultConnection ();
$this -> setConnection ( $name );
return tap ( $callback (), function () use ( $previousConnection ) {
$this -> setConnection ( $previousConnection );
});
}
/**
* Set the default connection name .
*
* @ param string $name
* @ return void
*/
public function setConnection ( $name )
{
if ( ! is_null ( $name )) {
$this -> resolver -> setDefaultConnection ( $name );
}
$this -> repository -> setSource ( $name );
$this -> connection = $name ;
}
/**
* Resolve the database connection instance .
*
* @ param string $connection
* @ return \Illuminate\Database\Connection
*/
public function resolveConnection ( $connection )
{
return $this -> resolver -> connection ( $connection ? : $this -> connection );
}
/**
* Get the schema grammar out of a migration connection .
*
* @ param \Illuminate\Database\Connection $connection
* @ return \Illuminate\Database\Schema\Grammars\Grammar
*/
protected function getSchemaGrammar ( $connection )
{
if ( is_null ( $grammar = $connection -> getSchemaGrammar ())) {
$connection -> useDefaultSchemaGrammar ();
$grammar = $connection -> getSchemaGrammar ();
}
return $grammar ;
}
/**
* Get the migration repository instance .
*
* @ return \Illuminate\Database\Migrations\MigrationRepositoryInterface
*/
public function getRepository ()
{
return $this -> repository ;
}
/**
* Determine if the migration repository exists .
*
* @ return bool
*/
public function repositoryExists ()
{
return $this -> repository -> repositoryExists ();
}
/**
* Determine if any migrations have been run .
*
* @ return bool
*/
public function hasRunAnyMigrations ()
{
return $this -> repositoryExists () && count ( $this -> repository -> getRan ()) > 0 ;
}
/**
* Delete the migration repository data store .
*
* @ return void
*/
public function deleteRepository ()
{
return $this -> repository -> deleteRepository ();
}
/**
* Get the file system instance .
*
* @ return \Illuminate\Filesystem\Filesystem
*/
public function getFilesystem ()
{
return $this -> files ;
}
/**
* Set the output implementation that should be used by the console .
*
* @ param \Symfony\Component\Console\Output\OutputInterface $output
* @ return $this
*/
public function setOutput ( OutputInterface $output )
{
$this -> output = $output ;
return $this ;
}
/**
2023-02-24 06:26:40 -05:00
* Write to the console ' s output .
2021-08-27 06:46:27 -04:00
*
2023-02-24 06:26:40 -05:00
* @ param string $component
* @ param array < int , string >| string ... $arguments
2021-08-27 06:46:27 -04:00
* @ return void
*/
2023-02-24 06:26:40 -05:00
protected function write ( $component , ... $arguments )
2021-08-27 06:46:27 -04:00
{
2023-02-24 06:26:40 -05:00
if ( $this -> output && class_exists ( $component )) {
( new $component ( $this -> output )) -> render ( ... $arguments );
} else {
foreach ( $arguments as $argument ) {
if ( is_callable ( $argument )) {
$argument ();
}
}
2021-08-27 06:46:27 -04:00
}
}
/**
* Fire the given event for the migration .
*
* @ param \Illuminate\Contracts\Database\Events\MigrationEvent $event
* @ return void
*/
public function fireMigrationEvent ( $event )
{
if ( $this -> events ) {
$this -> events -> dispatch ( $event );
}
}
}