vendor and env first commit

This commit is contained in:
2025-03-28 08:52:46 +01:00
parent f8388bc81b
commit 8f26283832
10976 changed files with 1349952 additions and 2 deletions
@@ -0,0 +1,86 @@
<?php
namespace Illuminate\Database\Console;
use Illuminate\Console\Command;
use Illuminate\Database\ConnectionInterface;
use Illuminate\Database\MariaDbConnection;
use Illuminate\Database\MySqlConnection;
use Illuminate\Database\PostgresConnection;
use Illuminate\Database\SQLiteConnection;
use Illuminate\Database\SqlServerConnection;
use Illuminate\Support\Arr;
abstract class DatabaseInspectionCommand extends Command
{
/**
* Get a human-readable name for the given connection.
*
* @param \Illuminate\Database\ConnectionInterface $connection
* @param string $database
* @return string
*/
protected function getConnectionName(ConnectionInterface $connection, $database)
{
return match (true) {
$connection instanceof MySqlConnection && $connection->isMaria() => 'MariaDB',
$connection instanceof MySqlConnection => 'MySQL',
$connection instanceof MariaDbConnection => 'MariaDB',
$connection instanceof PostgresConnection => 'PostgreSQL',
$connection instanceof SQLiteConnection => 'SQLite',
$connection instanceof SqlServerConnection => 'SQL Server',
default => $database,
};
}
/**
* Get the number of open connections for a database.
*
* @param \Illuminate\Database\ConnectionInterface $connection
* @return int|null
*/
protected function getConnectionCount(ConnectionInterface $connection)
{
$result = match (true) {
$connection instanceof MySqlConnection => $connection->selectOne('show status where variable_name = "threads_connected"'),
$connection instanceof PostgresConnection => $connection->selectOne('select count(*) as "Value" from pg_stat_activity'),
$connection instanceof SqlServerConnection => $connection->selectOne('select count(*) Value from sys.dm_exec_sessions where status = ?', ['running']),
default => null,
};
if (! $result) {
return null;
}
return Arr::wrap((array) $result)['Value'];
}
/**
* Get the connection configuration details for the given connection.
*
* @param string $database
* @return array
*/
protected function getConfigFromDatabase($database)
{
$database ??= config('database.default');
return Arr::except(config('database.connections.'.$database), ['password']);
}
/**
* Remove the table prefix from a table name, if it exists.
*
* @param \Illuminate\Database\ConnectionInterface $connection
* @param string $table
* @return string
*/
protected function withoutTablePrefix(ConnectionInterface $connection, string $table)
{
$prefix = $connection->getTablePrefix();
return str_starts_with($table, $prefix)
? substr($table, strlen($prefix))
: $table;
}
}
@@ -0,0 +1,242 @@
<?php
namespace Illuminate\Database\Console;
use Illuminate\Console\Command;
use Illuminate\Support\ConfigurationUrlParser;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Process\Process;
use UnexpectedValueException;
#[AsCommand(name: 'db')]
class DbCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'db {connection? : The database connection that should be used}
{--read : Connect to the read connection}
{--write : Connect to the write connection}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Start a new database CLI session';
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
$connection = $this->getConnection();
if (! isset($connection['host']) && $connection['driver'] !== 'sqlite') {
$this->components->error('No host specified for this database connection.');
$this->line(' Use the <options=bold>[--read]</> and <options=bold>[--write]</> options to specify a read or write connection.');
$this->newLine();
return Command::FAILURE;
}
(new Process(
array_merge([$this->getCommand($connection)], $this->commandArguments($connection)),
null,
$this->commandEnvironment($connection)
))->setTimeout(null)->setTty(true)->mustRun(function ($type, $buffer) {
$this->output->write($buffer);
});
return 0;
}
/**
* Get the database connection configuration.
*
* @return array
*
* @throws \UnexpectedValueException
*/
public function getConnection()
{
$connection = $this->laravel['config']['database.connections.'.
(($db = $this->argument('connection')) ?? $this->laravel['config']['database.default'])
];
if (empty($connection)) {
throw new UnexpectedValueException("Invalid database connection [{$db}].");
}
if (! empty($connection['url'])) {
$connection = (new ConfigurationUrlParser)->parseConfiguration($connection);
}
if ($this->option('read')) {
if (is_array($connection['read']['host'])) {
$connection['read']['host'] = $connection['read']['host'][0];
}
$connection = array_merge($connection, $connection['read']);
} elseif ($this->option('write')) {
if (is_array($connection['write']['host'])) {
$connection['write']['host'] = $connection['write']['host'][0];
}
$connection = array_merge($connection, $connection['write']);
}
return $connection;
}
/**
* Get the arguments for the database client command.
*
* @param array $connection
* @return array
*/
public function commandArguments(array $connection)
{
$driver = ucfirst($connection['driver']);
return $this->{"get{$driver}Arguments"}($connection);
}
/**
* Get the environment variables for the database client command.
*
* @param array $connection
* @return array|null
*/
public function commandEnvironment(array $connection)
{
$driver = ucfirst($connection['driver']);
if (method_exists($this, "get{$driver}Environment")) {
return $this->{"get{$driver}Environment"}($connection);
}
return null;
}
/**
* Get the database client command to run.
*
* @param array $connection
* @return string
*/
public function getCommand(array $connection)
{
return [
'mysql' => 'mysql',
'mariadb' => 'mysql',
'pgsql' => 'psql',
'sqlite' => 'sqlite3',
'sqlsrv' => 'sqlcmd',
][$connection['driver']];
}
/**
* Get the arguments for the MySQL CLI.
*
* @param array $connection
* @return array
*/
protected function getMysqlArguments(array $connection)
{
return array_merge([
'--host='.$connection['host'],
'--port='.$connection['port'],
'--user='.$connection['username'],
], $this->getOptionalArguments([
'password' => '--password='.$connection['password'],
'unix_socket' => '--socket='.($connection['unix_socket'] ?? ''),
'charset' => '--default-character-set='.($connection['charset'] ?? ''),
], $connection), [$connection['database']]);
}
/**
* Get the arguments for the MariaDB CLI.
*
* @param array $connection
* @return array
*/
protected function getMariaDbArguments(array $connection)
{
return $this->getMysqlArguments($connection);
}
/**
* Get the arguments for the Postgres CLI.
*
* @param array $connection
* @return array
*/
protected function getPgsqlArguments(array $connection)
{
return [$connection['database']];
}
/**
* Get the arguments for the SQLite CLI.
*
* @param array $connection
* @return array
*/
protected function getSqliteArguments(array $connection)
{
return [$connection['database']];
}
/**
* Get the arguments for the SQL Server CLI.
*
* @param array $connection
* @return array
*/
protected function getSqlsrvArguments(array $connection)
{
return array_merge(...$this->getOptionalArguments([
'database' => ['-d', $connection['database']],
'username' => ['-U', $connection['username']],
'password' => ['-P', $connection['password']],
'host' => ['-S', 'tcp:'.$connection['host']
.($connection['port'] ? ','.$connection['port'] : ''), ],
'trust_server_certificate' => ['-C'],
], $connection));
}
/**
* Get the environment variables for the Postgres CLI.
*
* @param array $connection
* @return array|null
*/
protected function getPgsqlEnvironment(array $connection)
{
return array_merge(...$this->getOptionalArguments([
'username' => ['PGUSER' => $connection['username']],
'host' => ['PGHOST' => $connection['host']],
'port' => ['PGPORT' => $connection['port']],
'password' => ['PGPASSWORD' => $connection['password']],
], $connection));
}
/**
* Get the optional arguments based on the connection configuration.
*
* @param array $args
* @param array $connection
* @return array
*/
protected function getOptionalArguments(array $args, array $connection)
{
return array_values(array_filter($args, function ($key) use ($connection) {
return ! empty($connection[$key]);
}, ARRAY_FILTER_USE_KEY));
}
}
@@ -0,0 +1,94 @@
<?php
namespace Illuminate\Database\Console;
use Illuminate\Console\Command;
use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Database\Connection;
use Illuminate\Database\ConnectionResolverInterface;
use Illuminate\Database\Events\SchemaDumped;
use Illuminate\Filesystem\Filesystem;
use Illuminate\Support\Facades\Config;
use Symfony\Component\Console\Attribute\AsCommand;
#[AsCommand(name: 'schema:dump')]
class DumpCommand extends Command
{
/**
* The console command name.
*
* @var string
*/
protected $signature = 'schema:dump
{--database= : The database connection to use}
{--path= : The path where the schema dump file should be stored}
{--prune : Delete all existing migration files}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Dump the given database schema';
/**
* Execute the console command.
*
* @param \Illuminate\Database\ConnectionResolverInterface $connections
* @param \Illuminate\Contracts\Events\Dispatcher $dispatcher
* @return void
*/
public function handle(ConnectionResolverInterface $connections, Dispatcher $dispatcher)
{
$connection = $connections->connection($database = $this->input->getOption('database'));
$this->schemaState($connection)->dump(
$connection, $path = $this->path($connection)
);
$dispatcher->dispatch(new SchemaDumped($connection, $path));
$info = 'Database schema dumped';
if ($this->option('prune')) {
(new Filesystem)->deleteDirectory(
database_path('migrations'), $preserve = false
);
$info .= ' and pruned';
}
$this->components->info($info.' successfully.');
}
/**
* Create a schema state instance for the given connection.
*
* @param \Illuminate\Database\Connection $connection
* @return mixed
*/
protected function schemaState(Connection $connection)
{
$migrations = Config::get('database.migrations', 'migrations');
$migrationTable = is_array($migrations) ? ($migrations['table'] ?? 'migrations') : $migrations;
return $connection->getSchemaState()
->withMigrationTable($migrationTable)
->handleOutputUsing(function ($type, $buffer) {
$this->output->write($buffer);
});
}
/**
* Get the path that the dump should be written to.
*
* @param \Illuminate\Database\Connection $connection
*/
protected function path(Connection $connection)
{
return tap($this->option('path') ?: database_path('schema/'.$connection->getName().'-schema.sql'), function ($path) {
(new Filesystem)->ensureDirectoryExists(dirname($path));
});
}
}
@@ -0,0 +1,143 @@
<?php
namespace Illuminate\Database\Console\Factories;
use Illuminate\Console\GeneratorCommand;
use Illuminate\Support\Str;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputOption;
#[AsCommand(name: 'make:factory')]
class FactoryMakeCommand extends GeneratorCommand
{
/**
* The console command name.
*
* @var string
*/
protected $name = 'make:factory';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Create a new model factory';
/**
* The type of class being generated.
*
* @var string
*/
protected $type = 'Factory';
/**
* Get the stub file for the generator.
*
* @return string
*/
protected function getStub()
{
return $this->resolveStubPath('/stubs/factory.stub');
}
/**
* Resolve the fully-qualified path to the stub.
*
* @param string $stub
* @return string
*/
protected function resolveStubPath($stub)
{
return file_exists($customPath = $this->laravel->basePath(trim($stub, '/')))
? $customPath
: __DIR__.$stub;
}
/**
* Build the class with the given name.
*
* @param string $name
* @return string
*/
protected function buildClass($name)
{
$factory = class_basename(Str::ucfirst(str_replace('Factory', '', $name)));
$namespaceModel = $this->option('model')
? $this->qualifyModel($this->option('model'))
: $this->qualifyModel($this->guessModelName($name));
$model = class_basename($namespaceModel);
$namespace = $this->getNamespace(
Str::replaceFirst($this->rootNamespace(), 'Database\\Factories\\', $this->qualifyClass($this->getNameInput()))
);
$replace = [
'{{ factoryNamespace }}' => $namespace,
'NamespacedDummyModel' => $namespaceModel,
'{{ namespacedModel }}' => $namespaceModel,
'{{namespacedModel}}' => $namespaceModel,
'DummyModel' => $model,
'{{ model }}' => $model,
'{{model}}' => $model,
'{{ factory }}' => $factory,
'{{factory}}' => $factory,
];
return str_replace(
array_keys($replace), array_values($replace), parent::buildClass($name)
);
}
/**
* Get the destination class path.
*
* @param string $name
* @return string
*/
protected function getPath($name)
{
$name = (string) Str::of($name)->replaceFirst($this->rootNamespace(), '')->finish('Factory');
return $this->laravel->databasePath().'/factories/'.str_replace('\\', '/', $name).'.php';
}
/**
* Guess the model name from the Factory name or return a default model name.
*
* @param string $name
* @return string
*/
protected function guessModelName($name)
{
if (str_ends_with($name, 'Factory')) {
$name = substr($name, 0, -7);
}
$modelName = $this->qualifyModel(Str::after($name, $this->rootNamespace()));
if (class_exists($modelName)) {
return $modelName;
}
if (is_dir(app_path('Models/'))) {
return $this->rootNamespace().'Models\Model';
}
return $this->rootNamespace().'Model';
}
/**
* Get the console command options.
*
* @return array
*/
protected function getOptions()
{
return [
['model', 'm', InputOption::VALUE_OPTIONAL, 'The name of the model'],
];
}
}
@@ -0,0 +1,23 @@
<?php
namespace {{ factoryNamespace }};
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\{{ namespacedModel }}>
*/
class {{ factory }}Factory extends Factory
{
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array
{
return [
//
];
}
}
@@ -0,0 +1,51 @@
<?php
namespace Illuminate\Database\Console\Migrations;
use Illuminate\Console\Command;
class BaseCommand extends Command
{
/**
* Get all of the migration paths.
*
* @return string[]
*/
protected function getMigrationPaths()
{
// Here, we will check to see if a path option has been defined. If it has we will
// use the path relative to the root of the installation folder so our database
// migrations may be run for any customized path from within the application.
if ($this->input->hasOption('path') && $this->option('path')) {
return collect($this->option('path'))->map(function ($path) {
return ! $this->usingRealPath()
? $this->laravel->basePath().'/'.$path
: $path;
})->all();
}
return array_merge(
$this->migrator->paths(), [$this->getMigrationPath()]
);
}
/**
* Determine if the given path(s) are pre-resolved "real" paths.
*
* @return bool
*/
protected function usingRealPath()
{
return $this->input->hasOption('realpath') && $this->option('realpath');
}
/**
* Get the path to the migration directory.
*
* @return string
*/
protected function getMigrationPath()
{
return $this->laravel->databasePath().DIRECTORY_SEPARATOR.'migrations';
}
}
@@ -0,0 +1,149 @@
<?php
namespace Illuminate\Database\Console\Migrations;
use Illuminate\Console\Command;
use Illuminate\Console\ConfirmableTrait;
use Illuminate\Console\Prohibitable;
use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Database\Events\DatabaseRefreshed;
use Illuminate\Database\Migrations\Migrator;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputOption;
#[AsCommand(name: 'migrate:fresh')]
class FreshCommand extends Command
{
use ConfirmableTrait, Prohibitable;
/**
* The console command name.
*
* @var string
*/
protected $name = 'migrate:fresh';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Drop all tables and re-run all migrations';
/**
* The migrator instance.
*
* @var \Illuminate\Database\Migrations\Migrator
*/
protected $migrator;
/**
* Create a new fresh command instance.
*
* @param \Illuminate\Database\Migrations\Migrator $migrator
* @return void
*/
public function __construct(Migrator $migrator)
{
parent::__construct();
$this->migrator = $migrator;
}
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
if ($this->isProhibited() ||
! $this->confirmToProceed()) {
return Command::FAILURE;
}
$database = $this->input->getOption('database');
$this->migrator->usingConnection($database, function () use ($database) {
if ($this->migrator->repositoryExists()) {
$this->newLine();
$this->components->task('Dropping all tables', fn () => $this->callSilent('db:wipe', array_filter([
'--database' => $database,
'--drop-views' => $this->option('drop-views'),
'--drop-types' => $this->option('drop-types'),
'--force' => true,
])) == 0);
}
});
$this->newLine();
$this->call('migrate', array_filter([
'--database' => $database,
'--path' => $this->input->getOption('path'),
'--realpath' => $this->input->getOption('realpath'),
'--schema-path' => $this->input->getOption('schema-path'),
'--force' => true,
'--step' => $this->option('step'),
]));
if ($this->laravel->bound(Dispatcher::class)) {
$this->laravel[Dispatcher::class]->dispatch(
new DatabaseRefreshed($database, $this->needsSeeding())
);
}
if ($this->needsSeeding()) {
$this->runSeeder($database);
}
return 0;
}
/**
* Determine if the developer has requested database seeding.
*
* @return bool
*/
protected function needsSeeding()
{
return $this->option('seed') || $this->option('seeder');
}
/**
* Run the database seeder command.
*
* @param string $database
* @return void
*/
protected function runSeeder($database)
{
$this->call('db:seed', array_filter([
'--database' => $database,
'--class' => $this->option('seeder') ?: 'Database\\Seeders\\DatabaseSeeder',
'--force' => true,
]));
}
/**
* Get the console command options.
*
* @return array
*/
protected function getOptions()
{
return [
['database', null, InputOption::VALUE_OPTIONAL, 'The database connection to use'],
['drop-views', null, InputOption::VALUE_NONE, 'Drop all tables and views'],
['drop-types', null, InputOption::VALUE_NONE, 'Drop all tables and types (Postgres only)'],
['force', null, InputOption::VALUE_NONE, 'Force the operation to run when in production'],
['path', null, InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 'The path(s) to the migrations files to be executed'],
['realpath', null, InputOption::VALUE_NONE, 'Indicate any provided migration file paths are pre-resolved absolute paths'],
['schema-path', null, InputOption::VALUE_OPTIONAL, 'The path to a schema dump file'],
['seed', null, InputOption::VALUE_NONE, 'Indicates if the seed task should be re-run'],
['seeder', null, InputOption::VALUE_OPTIONAL, 'The class name of the root seeder'],
['step', null, InputOption::VALUE_NONE, 'Force the migrations to be run so they can be rolled back individually'],
];
}
}
@@ -0,0 +1,72 @@
<?php
namespace Illuminate\Database\Console\Migrations;
use Illuminate\Console\Command;
use Illuminate\Database\Migrations\MigrationRepositoryInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputOption;
#[AsCommand(name: 'migrate:install')]
class InstallCommand extends Command
{
/**
* The console command name.
*
* @var string
*/
protected $name = 'migrate:install';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Create the migration repository';
/**
* The repository instance.
*
* @var \Illuminate\Database\Migrations\MigrationRepositoryInterface
*/
protected $repository;
/**
* Create a new migration install command instance.
*
* @param \Illuminate\Database\Migrations\MigrationRepositoryInterface $repository
* @return void
*/
public function __construct(MigrationRepositoryInterface $repository)
{
parent::__construct();
$this->repository = $repository;
}
/**
* Execute the console command.
*
* @return void
*/
public function handle()
{
$this->repository->setSource($this->input->getOption('database'));
$this->repository->createRepository();
$this->components->info('Migration table created successfully.');
}
/**
* Get the console command options.
*
* @return array
*/
protected function getOptions()
{
return [
['database', null, InputOption::VALUE_OPTIONAL, 'The database connection to use'],
];
}
}
@@ -0,0 +1,315 @@
<?php
namespace Illuminate\Database\Console\Migrations;
use Illuminate\Console\ConfirmableTrait;
use Illuminate\Contracts\Console\Isolatable;
use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Database\Events\SchemaLoaded;
use Illuminate\Database\Migrations\Migrator;
use Illuminate\Database\SQLiteDatabaseDoesNotExistException;
use Illuminate\Database\SqlServerConnection;
use PDOException;
use RuntimeException;
use Symfony\Component\Console\Attribute\AsCommand;
use Throwable;
use function Laravel\Prompts\confirm;
#[AsCommand(name: 'migrate')]
class MigrateCommand extends BaseCommand implements Isolatable
{
use ConfirmableTrait;
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'migrate {--database= : The database connection to use}
{--force : Force the operation to run when in production}
{--path=* : The path(s) to the migrations files to be executed}
{--realpath : Indicate any provided migration file paths are pre-resolved absolute paths}
{--schema-path= : The path to a schema dump file}
{--pretend : Dump the SQL queries that would be run}
{--seed : Indicates if the seed task should be re-run}
{--seeder= : The class name of the root seeder}
{--step : Force the migrations to be run so they can be rolled back individually}
{--graceful : Return a successful exit code even if an error occurs}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Run the database migrations';
/**
* The migrator instance.
*
* @var \Illuminate\Database\Migrations\Migrator
*/
protected $migrator;
/**
* The event dispatcher instance.
*
* @var \Illuminate\Contracts\Events\Dispatcher
*/
protected $dispatcher;
/**
* Create a new migration command instance.
*
* @param \Illuminate\Database\Migrations\Migrator $migrator
* @param \Illuminate\Contracts\Events\Dispatcher $dispatcher
* @return void
*/
public function __construct(Migrator $migrator, Dispatcher $dispatcher)
{
parent::__construct();
$this->migrator = $migrator;
$this->dispatcher = $dispatcher;
}
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
if (! $this->confirmToProceed()) {
return 1;
}
try {
$this->runMigrations();
} catch (Throwable $e) {
if ($this->option('graceful')) {
$this->components->warn($e->getMessage());
return 0;
}
throw $e;
}
return 0;
}
/**
* Run the pending migrations.
*
* @return void
*/
protected function runMigrations()
{
$this->migrator->usingConnection($this->option('database'), function () {
$this->prepareDatabase();
// Next, we will check to see if a path option has been defined. If it has
// we will use the path relative to the root of this installation folder
// so that migrations may be run for any path within the applications.
$this->migrator->setOutput($this->output)
->run($this->getMigrationPaths(), [
'pretend' => $this->option('pretend'),
'step' => $this->option('step'),
]);
// Finally, if the "seed" option has been given, we will re-run the database
// seed task to re-populate the database, which is convenient when adding
// a migration and a seed at the same time, as it is only this command.
if ($this->option('seed') && ! $this->option('pretend')) {
$this->call('db:seed', [
'--class' => $this->option('seeder') ?: 'Database\\Seeders\\DatabaseSeeder',
'--force' => true,
]);
}
});
}
/**
* Prepare the migration database for running.
*
* @return void
*/
protected function prepareDatabase()
{
if (! $this->repositoryExists()) {
$this->components->info('Preparing database.');
$this->components->task('Creating migration table', function () {
return $this->callSilent('migrate:install', array_filter([
'--database' => $this->option('database'),
])) == 0;
});
$this->newLine();
}
if (! $this->migrator->hasRunAnyMigrations() && ! $this->option('pretend')) {
$this->loadSchemaState();
}
}
/**
* Determine if the migrator repository exists.
*
* @return bool
*/
protected function repositoryExists()
{
return retry(2, fn () => $this->migrator->repositoryExists(), 0, function ($e) {
try {
if ($e->getPrevious() instanceof SQLiteDatabaseDoesNotExistException) {
return $this->createMissingSqliteDatabase($e->getPrevious()->path);
}
$connection = $this->migrator->resolveConnection($this->option('database'));
if (
$e->getPrevious() instanceof PDOException &&
$e->getPrevious()->getCode() === 1049 &&
in_array($connection->getDriverName(), ['mysql', 'mariadb'])) {
return $this->createMissingMysqlDatabase($connection);
}
return false;
} catch (Throwable) {
return false;
}
});
}
/**
* Create a missing SQLite database.
*
* @param string $path
* @return bool
*
* @throws \RuntimeException
*/
protected function createMissingSqliteDatabase($path)
{
if ($this->option('force')) {
return touch($path);
}
if ($this->option('no-interaction')) {
return false;
}
$this->components->warn('The SQLite database configured for this application does not exist: '.$path);
if (! confirm('Would you like to create it?', default: true)) {
$this->components->info('Operation cancelled. No database was created.');
throw new RuntimeException('Database was not created. Aborting migration.');
}
return touch($path);
}
/**
* Create a missing MySQL database.
*
* @return bool
*
* @throws \RuntimeException
*/
protected function createMissingMysqlDatabase($connection)
{
if ($this->laravel['config']->get("database.connections.{$connection->getName()}.database") !== $connection->getDatabaseName()) {
return false;
}
if (! $this->option('force') && $this->option('no-interaction')) {
return false;
}
if (! $this->option('force') && ! $this->option('no-interaction')) {
$this->components->warn("The database '{$connection->getDatabaseName()}' does not exist on the '{$connection->getName()}' connection.");
if (! confirm('Would you like to create it?', default: true)) {
$this->components->info('Operation cancelled. No database was created.');
throw new RuntimeException('Database was not created. Aborting migration.');
}
}
try {
$this->laravel['config']->set("database.connections.{$connection->getName()}.database", null);
$this->laravel['db']->purge();
$freshConnection = $this->migrator->resolveConnection($this->option('database'));
return tap($freshConnection->unprepared("CREATE DATABASE IF NOT EXISTS `{$connection->getDatabaseName()}`"), function () {
$this->laravel['db']->purge();
});
} finally {
$this->laravel['config']->set("database.connections.{$connection->getName()}.database", $connection->getDatabaseName());
}
}
/**
* Load the schema state to seed the initial database schema structure.
*
* @return void
*/
protected function loadSchemaState()
{
$connection = $this->migrator->resolveConnection($this->option('database'));
// First, we will make sure that the connection supports schema loading and that
// the schema file exists before we proceed any further. If not, we will just
// continue with the standard migration operation as normal without errors.
if ($connection instanceof SqlServerConnection ||
! is_file($path = $this->schemaPath($connection))) {
return;
}
$this->components->info('Loading stored database schemas.');
$this->components->task($path, function () use ($connection, $path) {
// Since the schema file will create the "migrations" table and reload it to its
// proper state, we need to delete it here so we don't get an error that this
// table already exists when the stored database schema file gets executed.
$this->migrator->deleteRepository();
$connection->getSchemaState()->handleOutputUsing(function ($type, $buffer) {
$this->output->write($buffer);
})->load($path);
});
$this->newLine();
// Finally, we will fire an event that this schema has been loaded so developers
// can perform any post schema load tasks that are necessary in listeners for
// this event, which may seed the database tables with some necessary data.
$this->dispatcher->dispatch(
new SchemaLoaded($connection, $path)
);
}
/**
* Get the path to the stored schema for the given connection.
*
* @param \Illuminate\Database\Connection $connection
* @return string
*/
protected function schemaPath($connection)
{
if ($this->option('schema-path')) {
return $this->option('schema-path');
}
if (file_exists($path = database_path('schema/'.$connection->getName().'-schema.dump'))) {
return $path;
}
return database_path('schema/'.$connection->getName().'-schema.sql');
}
}
@@ -0,0 +1,146 @@
<?php
namespace Illuminate\Database\Console\Migrations;
use Illuminate\Contracts\Console\PromptsForMissingInput;
use Illuminate\Database\Migrations\MigrationCreator;
use Illuminate\Support\Composer;
use Illuminate\Support\Str;
use Symfony\Component\Console\Attribute\AsCommand;
#[AsCommand(name: 'make:migration')]
class MigrateMakeCommand extends BaseCommand implements PromptsForMissingInput
{
/**
* The console command signature.
*
* @var string
*/
protected $signature = 'make:migration {name : The name of the migration}
{--create= : The table to be created}
{--table= : The table to migrate}
{--path= : The location where the migration file should be created}
{--realpath : Indicate any provided migration file paths are pre-resolved absolute paths}
{--fullpath : Output the full path of the migration (Deprecated)}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Create a new migration file';
/**
* The migration creator instance.
*
* @var \Illuminate\Database\Migrations\MigrationCreator
*/
protected $creator;
/**
* The Composer instance.
*
* @var \Illuminate\Support\Composer
*
* @deprecated Will be removed in a future Laravel version.
*/
protected $composer;
/**
* Create a new migration install command instance.
*
* @param \Illuminate\Database\Migrations\MigrationCreator $creator
* @param \Illuminate\Support\Composer $composer
* @return void
*/
public function __construct(MigrationCreator $creator, Composer $composer)
{
parent::__construct();
$this->creator = $creator;
$this->composer = $composer;
}
/**
* Execute the console command.
*
* @return void
*/
public function handle()
{
// It's possible for the developer to specify the tables to modify in this
// schema operation. The developer may also specify if this table needs
// to be freshly created so we can create the appropriate migrations.
$name = Str::snake(trim($this->input->getArgument('name')));
$table = $this->input->getOption('table');
$create = $this->input->getOption('create') ?: false;
// If no table was given as an option but a create option is given then we
// will use the "create" option as the table name. This allows the devs
// to pass a table name into this option as a short-cut for creating.
if (! $table && is_string($create)) {
$table = $create;
$create = true;
}
// Next, we will attempt to guess the table name if this the migration has
// "create" in the name. This will allow us to provide a convenient way
// of creating migrations that create new tables for the application.
if (! $table) {
[$table, $create] = TableGuesser::guess($name);
}
// Now we are ready to write the migration out to disk. Once we've written
// the migration out, we will dump-autoload for the entire framework to
// make sure that the migrations are registered by the class loaders.
$this->writeMigration($name, $table, $create);
}
/**
* Write the migration file to disk.
*
* @param string $name
* @param string $table
* @param bool $create
* @return void
*/
protected function writeMigration($name, $table, $create)
{
$file = $this->creator->create(
$name, $this->getMigrationPath(), $table, $create
);
$this->components->info(sprintf('Migration [%s] created successfully.', $file));
}
/**
* Get migration path (either specified by '--path' option or default location).
*
* @return string
*/
protected function getMigrationPath()
{
if (! is_null($targetPath = $this->input->getOption('path'))) {
return ! $this->usingRealPath()
? $this->laravel->basePath().'/'.$targetPath
: $targetPath;
}
return parent::getMigrationPath();
}
/**
* Prompt for missing input arguments using the returned questions.
*
* @return array
*/
protected function promptForMissingArgumentsUsing()
{
return [
'name' => ['What should the migration be named?', 'E.g. create_flights_table'],
];
}
}
@@ -0,0 +1,163 @@
<?php
namespace Illuminate\Database\Console\Migrations;
use Illuminate\Console\Command;
use Illuminate\Console\ConfirmableTrait;
use Illuminate\Console\Prohibitable;
use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Database\Events\DatabaseRefreshed;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputOption;
#[AsCommand(name: 'migrate:refresh')]
class RefreshCommand extends Command
{
use ConfirmableTrait, Prohibitable;
/**
* The console command name.
*
* @var string
*/
protected $name = 'migrate:refresh';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Reset and re-run all migrations';
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
if ($this->isProhibited() ||
! $this->confirmToProceed()) {
return 1;
}
// Next we'll gather some of the options so that we can have the right options
// to pass to the commands. This includes options such as which database to
// use and the path to use for the migration. Then we'll run the command.
$database = $this->input->getOption('database');
$path = $this->input->getOption('path');
// If the "step" option is specified it means we only want to rollback a small
// number of migrations before migrating again. For example, the user might
// only rollback and remigrate the latest four migrations instead of all.
$step = $this->input->getOption('step') ?: 0;
if ($step > 0) {
$this->runRollback($database, $path, $step);
} else {
$this->runReset($database, $path);
}
// The refresh command is essentially just a brief aggregate of a few other of
// the migration commands and just provides a convenient wrapper to execute
// them in succession. We'll also see if we need to re-seed the database.
$this->call('migrate', array_filter([
'--database' => $database,
'--path' => $path,
'--realpath' => $this->input->getOption('realpath'),
'--force' => true,
]));
if ($this->laravel->bound(Dispatcher::class)) {
$this->laravel[Dispatcher::class]->dispatch(
new DatabaseRefreshed($database, $this->needsSeeding())
);
}
if ($this->needsSeeding()) {
$this->runSeeder($database);
}
return 0;
}
/**
* Run the rollback command.
*
* @param string $database
* @param string $path
* @param int $step
* @return void
*/
protected function runRollback($database, $path, $step)
{
$this->call('migrate:rollback', array_filter([
'--database' => $database,
'--path' => $path,
'--realpath' => $this->input->getOption('realpath'),
'--step' => $step,
'--force' => true,
]));
}
/**
* Run the reset command.
*
* @param string $database
* @param string $path
* @return void
*/
protected function runReset($database, $path)
{
$this->call('migrate:reset', array_filter([
'--database' => $database,
'--path' => $path,
'--realpath' => $this->input->getOption('realpath'),
'--force' => true,
]));
}
/**
* Determine if the developer has requested database seeding.
*
* @return bool
*/
protected function needsSeeding()
{
return $this->option('seed') || $this->option('seeder');
}
/**
* Run the database seeder command.
*
* @param string $database
* @return void
*/
protected function runSeeder($database)
{
$this->call('db:seed', array_filter([
'--database' => $database,
'--class' => $this->option('seeder') ?: 'Database\\Seeders\\DatabaseSeeder',
'--force' => true,
]));
}
/**
* Get the console command options.
*
* @return array
*/
protected function getOptions()
{
return [
['database', null, InputOption::VALUE_OPTIONAL, 'The database connection to use'],
['force', null, InputOption::VALUE_NONE, 'Force the operation to run when in production'],
['path', null, InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 'The path(s) to the migrations files to be executed'],
['realpath', null, InputOption::VALUE_NONE, 'Indicate any provided migration file paths are pre-resolved absolute paths'],
['seed', null, InputOption::VALUE_NONE, 'Indicates if the seed task should be re-run'],
['seeder', null, InputOption::VALUE_OPTIONAL, 'The class name of the root seeder'],
['step', null, InputOption::VALUE_OPTIONAL, 'The number of migrations to be reverted & re-run'],
];
}
}
@@ -0,0 +1,95 @@
<?php
namespace Illuminate\Database\Console\Migrations;
use Illuminate\Console\ConfirmableTrait;
use Illuminate\Console\Prohibitable;
use Illuminate\Database\Migrations\Migrator;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputOption;
#[AsCommand(name: 'migrate:reset')]
class ResetCommand extends BaseCommand
{
use ConfirmableTrait, Prohibitable;
/**
* The console command name.
*
* @var string
*/
protected $name = 'migrate:reset';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Rollback all database migrations';
/**
* The migrator instance.
*
* @var \Illuminate\Database\Migrations\Migrator
*/
protected $migrator;
/**
* Create a new migration rollback command instance.
*
* @param \Illuminate\Database\Migrations\Migrator $migrator
* @return void
*/
public function __construct(Migrator $migrator)
{
parent::__construct();
$this->migrator = $migrator;
}
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
if ($this->isProhibited() ||
! $this->confirmToProceed()) {
return 1;
}
return $this->migrator->usingConnection($this->option('database'), function () {
// First, we'll make sure that the migration table actually exists before we
// start trying to rollback and re-run all of the migrations. If it's not
// present we'll just bail out with an info message for the developers.
if (! $this->migrator->repositoryExists()) {
return $this->components->warn('Migration table not found.');
}
$this->migrator->setOutput($this->output)->reset(
$this->getMigrationPaths(), $this->option('pretend')
);
});
}
/**
* Get the console command options.
*
* @return array
*/
protected function getOptions()
{
return [
['database', null, InputOption::VALUE_OPTIONAL, 'The database connection to use'],
['force', null, InputOption::VALUE_NONE, 'Force the operation to run when in production'],
['path', null, InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 'The path(s) to the migrations files to be executed'],
['realpath', null, InputOption::VALUE_NONE, 'Indicate any provided migration file paths are pre-resolved absolute paths'],
['pretend', null, InputOption::VALUE_NONE, 'Dump the SQL queries that would be run'],
];
}
}
@@ -0,0 +1,90 @@
<?php
namespace Illuminate\Database\Console\Migrations;
use Illuminate\Console\ConfirmableTrait;
use Illuminate\Database\Migrations\Migrator;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputOption;
#[AsCommand('migrate:rollback')]
class RollbackCommand extends BaseCommand
{
use ConfirmableTrait;
/**
* The console command name.
*
* @var string
*/
protected $name = 'migrate:rollback';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Rollback the last database migration';
/**
* The migrator instance.
*
* @var \Illuminate\Database\Migrations\Migrator
*/
protected $migrator;
/**
* Create a new migration rollback command instance.
*
* @param \Illuminate\Database\Migrations\Migrator $migrator
* @return void
*/
public function __construct(Migrator $migrator)
{
parent::__construct();
$this->migrator = $migrator;
}
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
if (! $this->confirmToProceed()) {
return 1;
}
$this->migrator->usingConnection($this->option('database'), function () {
$this->migrator->setOutput($this->output)->rollback(
$this->getMigrationPaths(), [
'pretend' => $this->option('pretend'),
'step' => (int) $this->option('step'),
'batch' => (int) $this->option('batch'),
]
);
});
return 0;
}
/**
* Get the console command options.
*
* @return array
*/
protected function getOptions()
{
return [
['database', null, InputOption::VALUE_OPTIONAL, 'The database connection to use'],
['force', null, InputOption::VALUE_NONE, 'Force the operation to run when in production'],
['path', null, InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 'The path(s) to the migrations files to be executed'],
['realpath', null, InputOption::VALUE_NONE, 'Indicate any provided migration file paths are pre-resolved absolute paths'],
['pretend', null, InputOption::VALUE_NONE, 'Dump the SQL queries that would be run'],
['step', null, InputOption::VALUE_OPTIONAL, 'The number of migrations to be reverted'],
['batch', null, InputOption::VALUE_REQUIRED, 'The batch of migrations (identified by their batch number) to be reverted'],
];
}
}
@@ -0,0 +1,142 @@
<?php
namespace Illuminate\Database\Console\Migrations;
use Illuminate\Database\Migrations\Migrator;
use Illuminate\Support\Collection;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputOption;
#[AsCommand(name: 'migrate:status')]
class StatusCommand extends BaseCommand
{
/**
* The console command name.
*
* @var string
*/
protected $name = 'migrate:status';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Show the status of each migration';
/**
* The migrator instance.
*
* @var \Illuminate\Database\Migrations\Migrator
*/
protected $migrator;
/**
* Create a new migration rollback command instance.
*
* @param \Illuminate\Database\Migrations\Migrator $migrator
* @return void
*/
public function __construct(Migrator $migrator)
{
parent::__construct();
$this->migrator = $migrator;
}
/**
* Execute the console command.
*
* @return int|null
*/
public function handle()
{
return $this->migrator->usingConnection($this->option('database'), function () {
if (! $this->migrator->repositoryExists()) {
$this->components->error('Migration table not found.');
return 1;
}
$ran = $this->migrator->getRepository()->getRan();
$batches = $this->migrator->getRepository()->getMigrationBatches();
$migrations = $this->getStatusFor($ran, $batches)
->when($this->option('pending') !== false, fn ($collection) => $collection->filter(function ($migration) {
return str($migration[1])->contains('Pending');
}));
if (count($migrations) > 0) {
$this->newLine();
$this->components->twoColumnDetail('<fg=gray>Migration name</>', '<fg=gray>Batch / Status</>');
$migrations
->each(
fn ($migration) => $this->components->twoColumnDetail($migration[0], $migration[1])
);
$this->newLine();
} elseif ($this->option('pending') !== false) {
$this->components->info('No pending migrations');
} else {
$this->components->info('No migrations found');
}
if ($this->option('pending') && $migrations->some(fn ($m) => str($m[1])->contains('Pending'))) {
return $this->option('pending');
}
});
}
/**
* Get the status for the given run migrations.
*
* @param array $ran
* @param array $batches
* @return \Illuminate\Support\Collection
*/
protected function getStatusFor(array $ran, array $batches)
{
return Collection::make($this->getAllMigrationFiles())
->map(function ($migration) use ($ran, $batches) {
$migrationName = $this->migrator->getMigrationName($migration);
$status = in_array($migrationName, $ran)
? '<fg=green;options=bold>Ran</>'
: '<fg=yellow;options=bold>Pending</>';
if (in_array($migrationName, $ran)) {
$status = '['.$batches[$migrationName].'] '.$status;
}
return [$migrationName, $status];
});
}
/**
* Get an array of all of the migration files.
*
* @return array
*/
protected function getAllMigrationFiles()
{
return $this->migrator->getMigrationFiles($this->getMigrationPaths());
}
/**
* Get the console command options.
*
* @return array
*/
protected function getOptions()
{
return [
['database', null, InputOption::VALUE_OPTIONAL, 'The database connection to use'],
['pending', null, InputOption::VALUE_OPTIONAL, 'Only list pending migrations', false],
['path', null, InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 'The path(s) to the migrations files to use'],
['realpath', null, InputOption::VALUE_NONE, 'Indicate any provided migration file paths are pre-resolved absolute paths'],
];
}
}
@@ -0,0 +1,37 @@
<?php
namespace Illuminate\Database\Console\Migrations;
class TableGuesser
{
const CREATE_PATTERNS = [
'/^create_(\w+)_table$/',
'/^create_(\w+)$/',
];
const CHANGE_PATTERNS = [
'/.+_(to|from|in)_(\w+)_table$/',
'/.+_(to|from|in)_(\w+)$/',
];
/**
* Attempt to guess the table name and "creation" status of the given migration.
*
* @param string $migration
* @return array
*/
public static function guess($migration)
{
foreach (self::CREATE_PATTERNS as $pattern) {
if (preg_match($pattern, $migration, $matches)) {
return [$matches[1], $create = true];
}
}
foreach (self::CHANGE_PATTERNS as $pattern) {
if (preg_match($pattern, $migration, $matches)) {
return [$matches[2], $create = false];
}
}
}
}
@@ -0,0 +1,138 @@
<?php
namespace Illuminate\Database\Console;
use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Database\ConnectionResolverInterface;
use Illuminate\Database\Events\DatabaseBusy;
use Symfony\Component\Console\Attribute\AsCommand;
#[AsCommand(name: 'db:monitor')]
class MonitorCommand extends DatabaseInspectionCommand
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'db:monitor
{--databases= : The database connections to monitor}
{--max= : The maximum number of connections that can be open before an event is dispatched}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Monitor the number of connections on the specified database';
/**
* The connection resolver instance.
*
* @var \Illuminate\Database\ConnectionResolverInterface
*/
protected $connection;
/**
* The events dispatcher instance.
*
* @var \Illuminate\Contracts\Events\Dispatcher
*/
protected $events;
/**
* Create a new command instance.
*
* @param \Illuminate\Database\ConnectionResolverInterface $connection
* @param \Illuminate\Contracts\Events\Dispatcher $events
*/
public function __construct(ConnectionResolverInterface $connection, Dispatcher $events)
{
parent::__construct();
$this->connection = $connection;
$this->events = $events;
}
/**
* Execute the console command.
*
* @return void
*/
public function handle()
{
$databases = $this->parseDatabases($this->option('databases'));
$this->displayConnections($databases);
if ($this->option('max')) {
$this->dispatchEvents($databases);
}
}
/**
* Parse the database into an array of the connections.
*
* @param string $databases
* @return \Illuminate\Support\Collection
*/
protected function parseDatabases($databases)
{
return collect(explode(',', $databases))->map(function ($database) {
if (! $database) {
$database = $this->laravel['config']['database.default'];
}
$maxConnections = $this->option('max');
return [
'database' => $database,
'connections' => $connections = $this->getConnectionCount($this->connection->connection($database)),
'status' => $maxConnections && $connections >= $maxConnections ? '<fg=yellow;options=bold>ALERT</>' : '<fg=green;options=bold>OK</>',
];
});
}
/**
* Display the databases and their connection counts in the console.
*
* @param \Illuminate\Support\Collection $databases
* @return void
*/
protected function displayConnections($databases)
{
$this->newLine();
$this->components->twoColumnDetail('<fg=gray>Database name</>', '<fg=gray>Connections</>');
$databases->each(function ($database) {
$status = '['.$database['connections'].'] '.$database['status'];
$this->components->twoColumnDetail($database['database'], $status);
});
$this->newLine();
}
/**
* Dispatch the database monitoring events.
*
* @param \Illuminate\Support\Collection $databases
* @return void
*/
protected function dispatchEvents($databases)
{
$databases->each(function ($database) {
if ($database['status'] === '<fg=green;options=bold>OK</>') {
return;
}
$this->events->dispatch(
new DatabaseBusy(
$database['database'],
$database['connections']
)
);
});
}
}
@@ -0,0 +1,201 @@
<?php
namespace Illuminate\Database\Console;
use Illuminate\Console\Command;
use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Database\Eloquent\MassPrunable;
use Illuminate\Database\Eloquent\Prunable;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Database\Events\ModelPruningFinished;
use Illuminate\Database\Events\ModelPruningStarting;
use Illuminate\Database\Events\ModelsPruned;
use Illuminate\Support\Str;
use InvalidArgumentException;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Finder\Finder;
#[AsCommand(name: 'model:prune')]
class PruneCommand extends Command
{
/**
* The console command name.
*
* @var string
*/
protected $signature = 'model:prune
{--model=* : Class names of the models to be pruned}
{--except=* : Class names of the models to be excluded from pruning}
{--path=* : Absolute path(s) to directories where models are located}
{--chunk=1000 : The number of models to retrieve per chunk of models to be deleted}
{--pretend : Display the number of prunable records found instead of deleting them}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Prune models that are no longer needed';
/**
* Execute the console command.
*
* @param \Illuminate\Contracts\Events\Dispatcher $events
* @return void
*/
public function handle(Dispatcher $events)
{
$models = $this->models();
if ($models->isEmpty()) {
$this->components->info('No prunable models found.');
return;
}
if ($this->option('pretend')) {
$models->each(function ($model) {
$this->pretendToPrune($model);
});
return;
}
$pruning = [];
$events->listen(ModelsPruned::class, function ($event) use (&$pruning) {
if (! in_array($event->model, $pruning)) {
$pruning[] = $event->model;
$this->newLine();
$this->components->info(sprintf('Pruning [%s] records.', $event->model));
}
$this->components->twoColumnDetail($event->model, "{$event->count} records");
});
$events->dispatch(new ModelPruningStarting($models->all()));
$models->each(function ($model) {
$this->pruneModel($model);
});
$events->dispatch(new ModelPruningFinished($models->all()));
$events->forget(ModelsPruned::class);
}
/**
* Prune the given model.
*
* @param string $model
* @return void
*/
protected function pruneModel(string $model)
{
$instance = new $model;
$chunkSize = property_exists($instance, 'prunableChunkSize')
? $instance->prunableChunkSize
: $this->option('chunk');
$total = $this->isPrunable($model)
? $instance->pruneAll($chunkSize)
: 0;
if ($total == 0) {
$this->components->info("No prunable [$model] records found.");
}
}
/**
* Determine the models that should be pruned.
*
* @return \Illuminate\Support\Collection
*/
protected function models()
{
if (! empty($models = $this->option('model'))) {
return collect($models)->filter(function ($model) {
return class_exists($model);
})->values();
}
$except = $this->option('except');
if (! empty($models) && ! empty($except)) {
throw new InvalidArgumentException('The --models and --except options cannot be combined.');
}
return collect(Finder::create()->in($this->getPath())->files()->name('*.php'))
->map(function ($model) {
$namespace = $this->laravel->getNamespace();
return $namespace.str_replace(
['/', '.php'],
['\\', ''],
Str::after($model->getRealPath(), realpath(app_path()).DIRECTORY_SEPARATOR)
);
})->when(! empty($except), function ($models) use ($except) {
return $models->reject(function ($model) use ($except) {
return in_array($model, $except);
});
})->filter(function ($model) {
return class_exists($model);
})->filter(function ($model) {
return $this->isPrunable($model);
})->values();
}
/**
* Get the path where models are located.
*
* @return string[]|string
*/
protected function getPath()
{
if (! empty($path = $this->option('path'))) {
return collect($path)->map(function ($path) {
return base_path($path);
})->all();
}
return app_path('Models');
}
/**
* Determine if the given model class is prunable.
*
* @param string $model
* @return bool
*/
protected function isPrunable($model)
{
$uses = class_uses_recursive($model);
return in_array(Prunable::class, $uses) || in_array(MassPrunable::class, $uses);
}
/**
* Display how many models will be pruned.
*
* @param string $model
* @return void
*/
protected function pretendToPrune($model)
{
$instance = new $model;
$count = $instance->prunable()
->when(in_array(SoftDeletes::class, class_uses_recursive(get_class($instance))), function ($query) {
$query->withTrashed();
})->count();
if ($count === 0) {
$this->components->info("No prunable [$model] records found.");
} else {
$this->components->info("{$count} [{$model}] records will be pruned.");
}
}
}
@@ -0,0 +1,140 @@
<?php
namespace Illuminate\Database\Console\Seeds;
use Illuminate\Console\Command;
use Illuminate\Console\ConfirmableTrait;
use Illuminate\Database\ConnectionResolverInterface as Resolver;
use Illuminate\Database\Eloquent\Model;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
#[AsCommand(name: 'db:seed')]
class SeedCommand extends Command
{
use ConfirmableTrait;
/**
* The console command name.
*
* @var string
*/
protected $name = 'db:seed';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Seed the database with records';
/**
* The connection resolver instance.
*
* @var \Illuminate\Database\ConnectionResolverInterface
*/
protected $resolver;
/**
* Create a new database seed command instance.
*
* @param \Illuminate\Database\ConnectionResolverInterface $resolver
* @return void
*/
public function __construct(Resolver $resolver)
{
parent::__construct();
$this->resolver = $resolver;
}
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
if (! $this->confirmToProceed()) {
return 1;
}
$this->components->info('Seeding database.');
$previousConnection = $this->resolver->getDefaultConnection();
$this->resolver->setDefaultConnection($this->getDatabase());
Model::unguarded(function () {
$this->getSeeder()->__invoke();
});
if ($previousConnection) {
$this->resolver->setDefaultConnection($previousConnection);
}
return 0;
}
/**
* Get a seeder instance from the container.
*
* @return \Illuminate\Database\Seeder
*/
protected function getSeeder()
{
$class = $this->input->getArgument('class') ?? $this->input->getOption('class');
if (! str_contains($class, '\\')) {
$class = 'Database\\Seeders\\'.$class;
}
if ($class === 'Database\\Seeders\\DatabaseSeeder' &&
! class_exists($class)) {
$class = 'DatabaseSeeder';
}
return $this->laravel->make($class)
->setContainer($this->laravel)
->setCommand($this);
}
/**
* Get the name of the database connection to use.
*
* @return string
*/
protected function getDatabase()
{
$database = $this->input->getOption('database');
return $database ?: $this->laravel['config']['database.default'];
}
/**
* Get the console command arguments.
*
* @return array
*/
protected function getArguments()
{
return [
['class', InputArgument::OPTIONAL, 'The class name of the root seeder', null],
];
}
/**
* Get the console command options.
*
* @return array
*/
protected function getOptions()
{
return [
['class', null, InputOption::VALUE_OPTIONAL, 'The class name of the root seeder', 'Database\\Seeders\\DatabaseSeeder'],
['database', null, InputOption::VALUE_OPTIONAL, 'The database connection to seed'],
['force', null, InputOption::VALUE_NONE, 'Force the operation to run when in production'],
];
}
}
@@ -0,0 +1,92 @@
<?php
namespace Illuminate\Database\Console\Seeds;
use Illuminate\Console\GeneratorCommand;
use Illuminate\Support\Str;
use Symfony\Component\Console\Attribute\AsCommand;
#[AsCommand(name: 'make:seeder')]
class SeederMakeCommand extends GeneratorCommand
{
/**
* The console command name.
*
* @var string
*/
protected $name = 'make:seeder';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Create a new seeder class';
/**
* The type of class being generated.
*
* @var string
*/
protected $type = 'Seeder';
/**
* Execute the console command.
*
* @return void
*/
public function handle()
{
parent::handle();
}
/**
* Get the stub file for the generator.
*
* @return string
*/
protected function getStub()
{
return $this->resolveStubPath('/stubs/seeder.stub');
}
/**
* Resolve the fully-qualified path to the stub.
*
* @param string $stub
* @return string
*/
protected function resolveStubPath($stub)
{
return is_file($customPath = $this->laravel->basePath(trim($stub, '/')))
? $customPath
: __DIR__.$stub;
}
/**
* Get the destination class path.
*
* @param string $name
* @return string
*/
protected function getPath($name)
{
$name = str_replace('\\', '/', Str::replaceFirst($this->rootNamespace(), '', $name));
if (is_dir($this->laravel->databasePath().'/seeds')) {
return $this->laravel->databasePath().'/seeds/'.$name.'.php';
}
return $this->laravel->databasePath().'/seeders/'.$name.'.php';
}
/**
* Get the root namespace for the class.
*
* @return string
*/
protected function rootNamespace()
{
return 'Database\Seeders\\';
}
}
@@ -0,0 +1,19 @@
<?php
namespace Illuminate\Database\Console\Seeds;
use Illuminate\Database\Eloquent\Model;
trait WithoutModelEvents
{
/**
* Prevent model events from being dispatched by the given callback.
*
* @param callable $callback
* @return callable
*/
public function withoutModelEvents(callable $callback)
{
return fn () => Model::withoutEvents($callback);
}
}
@@ -0,0 +1,17 @@
<?php
namespace {{ namespace }};
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
class {{ class }} extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
//
}
}
@@ -0,0 +1,238 @@
<?php
namespace Illuminate\Database\Console;
use Illuminate\Database\ConnectionInterface;
use Illuminate\Database\ConnectionResolverInterface;
use Illuminate\Database\Schema\Builder;
use Illuminate\Support\Arr;
use Illuminate\Support\Number;
use Symfony\Component\Console\Attribute\AsCommand;
#[AsCommand(name: 'db:show')]
class ShowCommand extends DatabaseInspectionCommand
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'db:show {--database= : The database connection}
{--json : Output the database information as JSON}
{--counts : Show the table row count <bg=red;options=bold> Note: This can be slow on large databases </>}
{--views : Show the database views <bg=red;options=bold> Note: This can be slow on large databases </>}
{--types : Show the user defined types}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Display information about the given database';
/**
* Execute the console command.
*
* @param \Illuminate\Database\ConnectionResolverInterface $connections
* @return int
*/
public function handle(ConnectionResolverInterface $connections)
{
$connection = $connections->connection($database = $this->input->getOption('database'));
$schema = $connection->getSchemaBuilder();
$data = [
'platform' => [
'config' => $this->getConfigFromDatabase($database),
'name' => $this->getConnectionName($connection, $database),
'version' => $connection->getServerVersion(),
'open_connections' => $this->getConnectionCount($connection),
],
'tables' => $this->tables($connection, $schema),
];
if ($this->option('views')) {
$data['views'] = $this->views($connection, $schema);
}
if ($this->option('types')) {
$data['types'] = $this->types($connection, $schema);
}
$this->display($data);
return 0;
}
/**
* Get information regarding the tables within the database.
*
* @param \Illuminate\Database\ConnectionInterface $connection
* @param \Illuminate\Database\Schema\Builder $schema
* @return \Illuminate\Support\Collection
*/
protected function tables(ConnectionInterface $connection, Builder $schema)
{
return collect($schema->getTables())->map(fn ($table) => [
'table' => $table['name'],
'schema' => $table['schema'],
'size' => $table['size'],
'rows' => $this->option('counts') ? $connection->table($table['name'])->count() : null,
'engine' => $table['engine'],
'collation' => $table['collation'],
'comment' => $table['comment'],
]);
}
/**
* Get information regarding the views within the database.
*
* @param \Illuminate\Database\ConnectionInterface $connection
* @param \Illuminate\Database\Schema\Builder $schema
* @return \Illuminate\Support\Collection
*/
protected function views(ConnectionInterface $connection, Builder $schema)
{
return collect($schema->getViews())
->reject(fn ($view) => str($view['name'])->startsWith(['pg_catalog', 'information_schema', 'spt_']))
->map(fn ($view) => [
'view' => $view['name'],
'schema' => $view['schema'],
'rows' => $connection->table($view->getName())->count(),
]);
}
/**
* Get information regarding the user-defined types within the database.
*
* @param \Illuminate\Database\ConnectionInterface $connection
* @param \Illuminate\Database\Schema\Builder $schema
* @return \Illuminate\Support\Collection
*/
protected function types(ConnectionInterface $connection, Builder $schema)
{
return collect($schema->getTypes())
->map(fn ($type) => [
'name' => $type['name'],
'schema' => $type['schema'],
'type' => $type['type'],
'category' => $type['category'],
]);
}
/**
* Render the database information.
*
* @param array $data
* @return void
*/
protected function display(array $data)
{
$this->option('json') ? $this->displayJson($data) : $this->displayForCli($data);
}
/**
* Render the database information as JSON.
*
* @param array $data
* @return void
*/
protected function displayJson(array $data)
{
$this->output->writeln(json_encode($data));
}
/**
* Render the database information formatted for the CLI.
*
* @param array $data
* @return void
*/
protected function displayForCli(array $data)
{
$platform = $data['platform'];
$tables = $data['tables'];
$views = $data['views'] ?? null;
$types = $data['types'] ?? null;
$this->newLine();
$this->components->twoColumnDetail('<fg=green;options=bold>'.$platform['name'].'</>', $platform['version']);
$this->components->twoColumnDetail('Database', Arr::get($platform['config'], 'database'));
$this->components->twoColumnDetail('Host', Arr::get($platform['config'], 'host'));
$this->components->twoColumnDetail('Port', Arr::get($platform['config'], 'port'));
$this->components->twoColumnDetail('Username', Arr::get($platform['config'], 'username'));
$this->components->twoColumnDetail('URL', Arr::get($platform['config'], 'url'));
$this->components->twoColumnDetail('Open Connections', $platform['open_connections']);
$this->components->twoColumnDetail('Tables', $tables->count());
if ($tableSizeSum = $tables->sum('size')) {
$this->components->twoColumnDetail('Total Size', Number::fileSize($tableSizeSum, 2));
}
$this->newLine();
if ($tables->isNotEmpty()) {
$hasSchema = ! is_null($tables->first()['schema']);
$this->components->twoColumnDetail(
($hasSchema ? '<fg=green;options=bold>Schema</> <fg=gray;options=bold>/</> ' : '').'<fg=green;options=bold>Table</>',
'Size'.($this->option('counts') ? ' <fg=gray;options=bold>/</> <fg=yellow;options=bold>Rows</>' : '')
);
$tables->each(function ($table) {
if ($tableSize = $table['size']) {
$tableSize = Number::fileSize($tableSize, 2);
}
$this->components->twoColumnDetail(
($table['schema'] ? $table['schema'].' <fg=gray;options=bold>/</> ' : '').$table['table'].($this->output->isVerbose() ? ' <fg=gray>'.$table['engine'].'</>' : null),
($tableSize ?: '—').($this->option('counts') ? ' <fg=gray;options=bold>/</> <fg=yellow;options=bold>'.Number::format($table['rows']).'</>' : '')
);
if ($this->output->isVerbose()) {
if ($table['comment']) {
$this->components->bulletList([
$table['comment'],
]);
}
}
});
$this->newLine();
}
if ($views && $views->isNotEmpty()) {
$hasSchema = ! is_null($views->first()['schema']);
$this->components->twoColumnDetail(
($hasSchema ? '<fg=green;options=bold>Schema</> <fg=gray;options=bold>/</> ' : '').'<fg=green;options=bold>View</>',
'<fg=green;options=bold>Rows</>'
);
$views->each(fn ($view) => $this->components->twoColumnDetail(
($view['schema'] ? $view['schema'].' <fg=gray;options=bold>/</> ' : '').$view['view'],
Number::format($view['rows'])
));
$this->newLine();
}
if ($types && $types->isNotEmpty()) {
$hasSchema = ! is_null($types->first()['schema']);
$this->components->twoColumnDetail(
($hasSchema ? '<fg=green;options=bold>Schema</> <fg=gray;options=bold>/</> ' : '').'<fg=green;options=bold>Type</>',
'<fg=green;options=bold>Type</> <fg=gray;options=bold>/</> <fg=green;options=bold>Category</>'
);
$types->each(fn ($type) => $this->components->twoColumnDetail(
($type['schema'] ? $type['schema'].' <fg=gray;options=bold>/</> ' : '').$type['name'],
$type['type'].' <fg=gray;options=bold>/</> '.$type['category']
));
$this->newLine();
}
}
}
@@ -0,0 +1,546 @@
<?php
namespace Illuminate\Database\Console;
use BackedEnum;
use Illuminate\Contracts\Container\BindingResolutionException;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Str;
use ReflectionClass;
use ReflectionMethod;
use ReflectionNamedType;
use SplFileObject;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Output\OutputInterface;
use UnitEnum;
#[AsCommand(name: 'model:show')]
class ShowModelCommand extends DatabaseInspectionCommand
{
/**
* The console command name.
*
* @var string
*/
protected $name = 'model:show {model}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Show information about an Eloquent model';
/**
* The console command signature.
*
* @var string
*/
protected $signature = 'model:show {model : The model to show}
{--database= : The database connection to use}
{--json : Output the model as JSON}';
/**
* The methods that can be called in a model to indicate a relation.
*
* @var array
*/
protected $relationMethods = [
'hasMany',
'hasManyThrough',
'hasOneThrough',
'belongsToMany',
'hasOne',
'belongsTo',
'morphOne',
'morphTo',
'morphMany',
'morphToMany',
'morphedByMany',
];
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
$class = $this->qualifyModel($this->argument('model'));
try {
$model = $this->laravel->make($class);
$class = get_class($model);
} catch (BindingResolutionException $e) {
$this->components->error($e->getMessage());
return 1;
}
if ($this->option('database')) {
$model->setConnection($this->option('database'));
}
$this->display(
$class,
$model->getConnection()->getName(),
$model->getConnection()->getTablePrefix().$model->getTable(),
$this->getPolicy($model),
$this->getAttributes($model),
$this->getRelations($model),
$this->getEvents($model),
$this->getObservers($model),
);
return 0;
}
/**
* Get the first policy associated with this model.
*
* @param \Illuminate\Database\Eloquent\Model $model
* @return string
*/
protected function getPolicy($model)
{
$policy = Gate::getPolicyFor($model::class);
return $policy ? $policy::class : null;
}
/**
* Get the column attributes for the given model.
*
* @param \Illuminate\Database\Eloquent\Model $model
* @return \Illuminate\Support\Collection
*/
protected function getAttributes($model)
{
$connection = $model->getConnection();
$schema = $connection->getSchemaBuilder();
$table = $model->getTable();
$columns = $schema->getColumns($table);
$indexes = $schema->getIndexes($table);
return collect($columns)
->map(fn ($column) => [
'name' => $column['name'],
'type' => $column['type'],
'increments' => $column['auto_increment'],
'nullable' => $column['nullable'],
'default' => $this->getColumnDefault($column, $model),
'unique' => $this->columnIsUnique($column['name'], $indexes),
'fillable' => $model->isFillable($column['name']),
'hidden' => $this->attributeIsHidden($column['name'], $model),
'appended' => null,
'cast' => $this->getCastType($column['name'], $model),
])
->merge($this->getVirtualAttributes($model, $columns));
}
/**
* Get the virtual (non-column) attributes for the given model.
*
* @param \Illuminate\Database\Eloquent\Model $model
* @param array $columns
* @return \Illuminate\Support\Collection
*/
protected function getVirtualAttributes($model, $columns)
{
$class = new ReflectionClass($model);
return collect($class->getMethods())
->reject(
fn (ReflectionMethod $method) => $method->isStatic()
|| $method->isAbstract()
|| $method->getDeclaringClass()->getName() === Model::class
)
->mapWithKeys(function (ReflectionMethod $method) use ($model) {
if (preg_match('/^get(.+)Attribute$/', $method->getName(), $matches) === 1) {
return [Str::snake($matches[1]) => 'accessor'];
} elseif ($model->hasAttributeMutator($method->getName())) {
return [Str::snake($method->getName()) => 'attribute'];
} else {
return [];
}
})
->reject(fn ($cast, $name) => collect($columns)->contains('name', $name))
->map(fn ($cast, $name) => [
'name' => $name,
'type' => null,
'increments' => false,
'nullable' => null,
'default' => null,
'unique' => null,
'fillable' => $model->isFillable($name),
'hidden' => $this->attributeIsHidden($name, $model),
'appended' => $model->hasAppended($name),
'cast' => $cast,
])
->values();
}
/**
* Get the relations from the given model.
*
* @param \Illuminate\Database\Eloquent\Model $model
* @return \Illuminate\Support\Collection
*/
protected function getRelations($model)
{
return collect(get_class_methods($model))
->map(fn ($method) => new ReflectionMethod($model, $method))
->reject(
fn (ReflectionMethod $method) => $method->isStatic()
|| $method->isAbstract()
|| $method->getDeclaringClass()->getName() === Model::class
|| $method->getNumberOfParameters() > 0
)
->filter(function (ReflectionMethod $method) {
if ($method->getReturnType() instanceof ReflectionNamedType
&& is_subclass_of($method->getReturnType()->getName(), Relation::class)) {
return true;
}
$file = new SplFileObject($method->getFileName());
$file->seek($method->getStartLine() - 1);
$code = '';
while ($file->key() < $method->getEndLine()) {
$code .= trim($file->current());
$file->next();
}
return collect($this->relationMethods)
->contains(fn ($relationMethod) => str_contains($code, '$this->'.$relationMethod.'('));
})
->map(function (ReflectionMethod $method) use ($model) {
$relation = $method->invoke($model);
if (! $relation instanceof Relation) {
return null;
}
return [
'name' => $method->getName(),
'type' => Str::afterLast(get_class($relation), '\\'),
'related' => get_class($relation->getRelated()),
];
})
->filter()
->values();
}
/**
* Get the Events that the model dispatches.
*
* @param \Illuminate\Database\Eloquent\Model $model
* @return \Illuminate\Support\Collection
*/
protected function getEvents($model)
{
return collect($model->dispatchesEvents())
->map(fn (string $class, string $event) => [
'event' => $event,
'class' => $class,
])->values();
}
/**
* Get the Observers watching this model.
*
* @param \Illuminate\Database\Eloquent\Model $model
* @return \Illuminate\Support\Collection
*/
protected function getObservers($model)
{
$listeners = $this->getLaravel()->make('events')->getRawListeners();
// Get the Eloquent observers for this model...
$listeners = array_filter($listeners, function ($v, $key) use ($model) {
return Str::startsWith($key, 'eloquent.') && Str::endsWith($key, $model::class);
}, ARRAY_FILTER_USE_BOTH);
// Format listeners Eloquent verb => Observer methods...
$extractVerb = function ($key) {
preg_match('/eloquent.([a-zA-Z]+)\: /', $key, $matches);
return $matches[1] ?? '?';
};
$formatted = [];
foreach ($listeners as $key => $observerMethods) {
$formatted[] = [
'event' => $extractVerb($key),
'observer' => array_map(fn ($obs) => is_string($obs) ? $obs : 'Closure', $observerMethods),
];
}
return collect($formatted);
}
/**
* Render the model information.
*
* @param string $class
* @param string $database
* @param string $table
* @param string $policy
* @param \Illuminate\Support\Collection $attributes
* @param \Illuminate\Support\Collection $relations
* @param \Illuminate\Support\Collection $events
* @param \Illuminate\Support\Collection $observers
* @return void
*/
protected function display($class, $database, $table, $policy, $attributes, $relations, $events, $observers)
{
$this->option('json')
? $this->displayJson($class, $database, $table, $policy, $attributes, $relations, $events, $observers)
: $this->displayCli($class, $database, $table, $policy, $attributes, $relations, $events, $observers);
}
/**
* Render the model information as JSON.
*
* @param string $class
* @param string $database
* @param string $table
* @param string $policy
* @param \Illuminate\Support\Collection $attributes
* @param \Illuminate\Support\Collection $relations
* @param \Illuminate\Support\Collection $events
* @param \Illuminate\Support\Collection $observers
* @return void
*/
protected function displayJson($class, $database, $table, $policy, $attributes, $relations, $events, $observers)
{
$this->output->writeln(
collect([
'class' => $class,
'database' => $database,
'table' => $table,
'policy' => $policy,
'attributes' => $attributes,
'relations' => $relations,
'events' => $events,
'observers' => $observers,
])->toJson()
);
}
/**
* Render the model information for the CLI.
*
* @param string $class
* @param string $database
* @param string $table
* @param string $policy
* @param \Illuminate\Support\Collection $attributes
* @param \Illuminate\Support\Collection $relations
* @param \Illuminate\Support\Collection $events
* @param \Illuminate\Support\Collection $observers
* @return void
*/
protected function displayCli($class, $database, $table, $policy, $attributes, $relations, $events, $observers)
{
$this->newLine();
$this->components->twoColumnDetail('<fg=green;options=bold>'.$class.'</>');
$this->components->twoColumnDetail('Database', $database);
$this->components->twoColumnDetail('Table', $table);
if ($policy) {
$this->components->twoColumnDetail('Policy', $policy);
}
$this->newLine();
$this->components->twoColumnDetail(
'<fg=green;options=bold>Attributes</>',
'type <fg=gray>/</> <fg=yellow;options=bold>cast</>',
);
foreach ($attributes as $attribute) {
$first = trim(sprintf(
'%s %s',
$attribute['name'],
collect(['increments', 'unique', 'nullable', 'fillable', 'hidden', 'appended'])
->filter(fn ($property) => $attribute[$property])
->map(fn ($property) => sprintf('<fg=gray>%s</>', $property))
->implode('<fg=gray>,</> ')
));
$second = collect([
$attribute['type'],
$attribute['cast'] ? '<fg=yellow;options=bold>'.$attribute['cast'].'</>' : null,
])->filter()->implode(' <fg=gray>/</> ');
$this->components->twoColumnDetail($first, $second);
if ($attribute['default'] !== null) {
$this->components->bulletList(
[sprintf('default: %s', $attribute['default'])],
OutputInterface::VERBOSITY_VERBOSE
);
}
}
$this->newLine();
$this->components->twoColumnDetail('<fg=green;options=bold>Relations</>');
foreach ($relations as $relation) {
$this->components->twoColumnDetail(
sprintf('%s <fg=gray>%s</>', $relation['name'], $relation['type']),
$relation['related']
);
}
$this->newLine();
$this->components->twoColumnDetail('<fg=green;options=bold>Events</>');
if ($events->count()) {
foreach ($events as $event) {
$this->components->twoColumnDetail(
sprintf('%s', $event['event']),
sprintf('%s', $event['class']),
);
}
}
$this->newLine();
$this->components->twoColumnDetail('<fg=green;options=bold>Observers</>');
if ($observers->count()) {
foreach ($observers as $observer) {
$this->components->twoColumnDetail(
sprintf('%s', $observer['event']),
implode(', ', $observer['observer'])
);
}
}
$this->newLine();
}
/**
* Get the cast type for the given column.
*
* @param string $column
* @param \Illuminate\Database\Eloquent\Model $model
* @return string|null
*/
protected function getCastType($column, $model)
{
if ($model->hasGetMutator($column) || $model->hasSetMutator($column)) {
return 'accessor';
}
if ($model->hasAttributeMutator($column)) {
return 'attribute';
}
return $this->getCastsWithDates($model)->get($column) ?? null;
}
/**
* Get the model casts, including any date casts.
*
* @param \Illuminate\Database\Eloquent\Model $model
* @return \Illuminate\Support\Collection
*/
protected function getCastsWithDates($model)
{
return collect($model->getDates())
->filter()
->flip()
->map(fn () => 'datetime')
->merge($model->getCasts());
}
/**
* Get the default value for the given column.
*
* @param array $column
* @param \Illuminate\Database\Eloquent\Model $model
* @return mixed|null
*/
protected function getColumnDefault($column, $model)
{
$attributeDefault = $model->getAttributes()[$column['name']] ?? null;
return match (true) {
$attributeDefault instanceof BackedEnum => $attributeDefault->value,
$attributeDefault instanceof UnitEnum => $attributeDefault->name,
default => $attributeDefault ?? $column['default'],
};
}
/**
* Determine if the given attribute is hidden.
*
* @param string $attribute
* @param \Illuminate\Database\Eloquent\Model $model
* @return bool
*/
protected function attributeIsHidden($attribute, $model)
{
if (count($model->getHidden()) > 0) {
return in_array($attribute, $model->getHidden());
}
if (count($model->getVisible()) > 0) {
return ! in_array($attribute, $model->getVisible());
}
return false;
}
/**
* Determine if the given attribute is unique.
*
* @param string $column
* @param array $indexes
* @return bool
*/
protected function columnIsUnique($column, $indexes)
{
return collect($indexes)->contains(
fn ($index) => count($index['columns']) === 1 && $index['columns'][0] === $column && $index['unique']
);
}
/**
* Qualify the given model class base name.
*
* @param string $model
* @return string
*
* @see \Illuminate\Console\GeneratorCommand
*/
protected function qualifyModel(string $model)
{
if (str_contains($model, '\\') && class_exists($model)) {
return $model;
}
$model = ltrim($model, '\\/');
$model = str_replace('/', '\\', $model);
$rootNamespace = $this->laravel->getNamespace();
if (Str::startsWith($model, $rootNamespace)) {
return $model;
}
return is_dir(app_path('Models'))
? $rootNamespace.'Models\\'.$model
: $rootNamespace.$model;
}
}
@@ -0,0 +1,248 @@
<?php
namespace Illuminate\Database\Console;
use Illuminate\Database\ConnectionResolverInterface;
use Illuminate\Database\Schema\Builder;
use Illuminate\Support\Arr;
use Illuminate\Support\Number;
use Symfony\Component\Console\Attribute\AsCommand;
use function Laravel\Prompts\select;
#[AsCommand(name: 'db:table')]
class TableCommand extends DatabaseInspectionCommand
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'db:table
{table? : The name of the table}
{--database= : The database connection}
{--json : Output the table information as JSON}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Display information about the given database table';
/**
* Execute the console command.
*
* @return int
*/
public function handle(ConnectionResolverInterface $connections)
{
$connection = $connections->connection($this->input->getOption('database'));
$schema = $connection->getSchemaBuilder();
$tables = $schema->getTables();
$tableName = $this->argument('table') ?: select(
'Which table would you like to inspect?',
array_column($tables, 'name')
);
$table = Arr::first($tables, fn ($table) => $table['name'] === $tableName);
if (! $table) {
$this->components->warn("Table [{$tableName}] doesn't exist.");
return 1;
}
$tableName = $this->withoutTablePrefix($connection, $table['name']);
$columns = $this->columns($schema, $tableName);
$indexes = $this->indexes($schema, $tableName);
$foreignKeys = $this->foreignKeys($schema, $tableName);
$data = [
'table' => [
'name' => $table['name'],
'columns' => count($columns),
'size' => $table['size'],
],
'columns' => $columns,
'indexes' => $indexes,
'foreign_keys' => $foreignKeys,
];
$this->display($data);
return 0;
}
/**
* Get the information regarding the table's columns.
*
* @param \Illuminate\Database\Schema\Builder $schema
* @param string $table
* @return \Illuminate\Support\Collection
*/
protected function columns(Builder $schema, string $table)
{
return collect($schema->getColumns($table))->map(fn ($column) => [
'column' => $column['name'],
'attributes' => $this->getAttributesForColumn($column),
'default' => $column['default'],
'type' => $column['type'],
]);
}
/**
* Get the attributes for a table column.
*
* @param array $column
* @return \Illuminate\Support\Collection
*/
protected function getAttributesForColumn($column)
{
return collect([
$column['type_name'],
$column['auto_increment'] ? 'autoincrement' : null,
$column['nullable'] ? 'nullable' : null,
$column['collation'],
])->filter();
}
/**
* Get the information regarding the table's indexes.
*
* @param \Illuminate\Database\Schema\Builder $schema
* @param string $table
* @return \Illuminate\Support\Collection
*/
protected function indexes(Builder $schema, string $table)
{
return collect($schema->getIndexes($table))->map(fn ($index) => [
'name' => $index['name'],
'columns' => collect($index['columns']),
'attributes' => $this->getAttributesForIndex($index),
]);
}
/**
* Get the attributes for a table index.
*
* @param array $index
* @return \Illuminate\Support\Collection
*/
protected function getAttributesForIndex($index)
{
return collect([
$index['type'],
count($index['columns']) > 1 ? 'compound' : null,
$index['unique'] && ! $index['primary'] ? 'unique' : null,
$index['primary'] ? 'primary' : null,
])->filter();
}
/**
* Get the information regarding the table's foreign keys.
*
* @param \Illuminate\Database\Schema\Builder $schema
* @param string $table
* @return \Illuminate\Support\Collection
*/
protected function foreignKeys(Builder $schema, string $table)
{
return collect($schema->getForeignKeys($table))->map(fn ($foreignKey) => [
'name' => $foreignKey['name'],
'columns' => collect($foreignKey['columns']),
'foreign_schema' => $foreignKey['foreign_schema'],
'foreign_table' => $foreignKey['foreign_table'],
'foreign_columns' => collect($foreignKey['foreign_columns']),
'on_update' => $foreignKey['on_update'],
'on_delete' => $foreignKey['on_delete'],
]);
}
/**
* Render the table information.
*
* @param array $data
* @return void
*/
protected function display(array $data)
{
$this->option('json') ? $this->displayJson($data) : $this->displayForCli($data);
}
/**
* Render the table information as JSON.
*
* @param array $data
* @return void
*/
protected function displayJson(array $data)
{
$this->output->writeln(json_encode($data));
}
/**
* Render the table information formatted for the CLI.
*
* @param array $data
* @return void
*/
protected function displayForCli(array $data)
{
[$table, $columns, $indexes, $foreignKeys] = [
$data['table'], $data['columns'], $data['indexes'], $data['foreign_keys'],
];
$this->newLine();
$this->components->twoColumnDetail('<fg=green;options=bold>'.$table['name'].'</>');
$this->components->twoColumnDetail('Columns', $table['columns']);
if ($size = $table['size']) {
$this->components->twoColumnDetail('Size', Number::fileSize($size, 2));
}
$this->newLine();
if ($columns->isNotEmpty()) {
$this->components->twoColumnDetail('<fg=green;options=bold>Column</>', 'Type');
$columns->each(function ($column) {
$this->components->twoColumnDetail(
$column['column'].' <fg=gray>'.$column['attributes']->implode(', ').'</>',
(! is_null($column['default']) ? '<fg=gray>'.$column['default'].'</> ' : '').$column['type']
);
});
$this->newLine();
}
if ($indexes->isNotEmpty()) {
$this->components->twoColumnDetail('<fg=green;options=bold>Index</>');
$indexes->each(function ($index) {
$this->components->twoColumnDetail(
$index['name'].' <fg=gray>'.$index['columns']->implode(', ').'</>',
$index['attributes']->implode(', ')
);
});
$this->newLine();
}
if ($foreignKeys->isNotEmpty()) {
$this->components->twoColumnDetail('<fg=green;options=bold>Foreign Key</>', 'On Update / On Delete');
$foreignKeys->each(function ($foreignKey) {
$this->components->twoColumnDetail(
$foreignKey['name'].' <fg=gray;options=bold>'.$foreignKey['columns']->implode(', ').' references '.$foreignKey['foreign_columns']->implode(', ').' on '.$foreignKey['foreign_table'].'</>',
$foreignKey['on_update'].' / '.$foreignKey['on_delete'],
);
});
$this->newLine();
}
}
}
@@ -0,0 +1,116 @@
<?php
namespace Illuminate\Database\Console;
use Illuminate\Console\Command;
use Illuminate\Console\ConfirmableTrait;
use Illuminate\Console\Prohibitable;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputOption;
#[AsCommand(name: 'db:wipe')]
class WipeCommand extends Command
{
use ConfirmableTrait, Prohibitable;
/**
* The console command name.
*
* @var string
*/
protected $name = 'db:wipe';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Drop all tables, views, and types';
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
if ($this->isProhibited() ||
! $this->confirmToProceed()) {
return Command::FAILURE;
}
$database = $this->input->getOption('database');
if ($this->option('drop-views')) {
$this->dropAllViews($database);
$this->components->info('Dropped all views successfully.');
}
$this->dropAllTables($database);
$this->components->info('Dropped all tables successfully.');
if ($this->option('drop-types')) {
$this->dropAllTypes($database);
$this->components->info('Dropped all types successfully.');
}
return 0;
}
/**
* Drop all of the database tables.
*
* @param string $database
* @return void
*/
protected function dropAllTables($database)
{
$this->laravel['db']->connection($database)
->getSchemaBuilder()
->dropAllTables();
}
/**
* Drop all of the database views.
*
* @param string $database
* @return void
*/
protected function dropAllViews($database)
{
$this->laravel['db']->connection($database)
->getSchemaBuilder()
->dropAllViews();
}
/**
* Drop all of the database types.
*
* @param string $database
* @return void
*/
protected function dropAllTypes($database)
{
$this->laravel['db']->connection($database)
->getSchemaBuilder()
->dropAllTypes();
}
/**
* Get the console command options.
*
* @return array
*/
protected function getOptions()
{
return [
['database', null, InputOption::VALUE_OPTIONAL, 'The database connection to use'],
['drop-views', null, InputOption::VALUE_NONE, 'Drop all tables and views'],
['drop-types', null, InputOption::VALUE_NONE, 'Drop all tables and types (Postgres only)'],
['force', null, InputOption::VALUE_NONE, 'Force the operation to run when in production'],
];
}
}