diff --git a/Console/Factories/FactoryMakeCommand.php b/Console/Factories/FactoryMakeCommand.php index ac97ae80e..88b9b4a0b 100644 --- a/Console/Factories/FactoryMakeCommand.php +++ b/Console/Factories/FactoryMakeCommand.php @@ -62,11 +62,18 @@ protected function buildClass($name) { $namespaceModel = $this->option('model') ? $this->qualifyModel($this->option('model')) - : $this->qualifyModel('Model'); + : $this->qualifyModel($this->guessModelName($name)); $model = class_basename($namespaceModel); + if (Str::startsWith($namespaceModel, 'App\\Models')) { + $namespace = Str::beforeLast('Database\\Factories\\'.Str::after($namespaceModel, 'App\\Models\\'), '\\'); + } else { + $namespace = 'Database\\Factories'; + } + $replace = [ + '{{ factoryNamespace }}' => $namespace, 'NamespacedDummyModel' => $namespaceModel, '{{ namespacedModel }}' => $namespaceModel, '{{namespacedModel}}' => $namespaceModel, @@ -88,13 +95,36 @@ protected function buildClass($name) */ protected function getPath($name) { - $name = str_replace( - ['\\', '/'], '', $this->argument('name') - ); + $name = Str::replaceFirst('App\\', '', $name); + + $name = Str::finish($this->argument('name'), '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::endsWith($name, 'Factory')) { + $name = substr($name, 0, -7); + } + + $modelName = $this->qualifyModel(class_basename($name)); + + if (class_exists($modelName)) { + return $modelName; + } - $name = Str::finish($name, 'Factory'); + if (is_dir(app_path('Models/'))) { + return 'App\Models\Model'; + } - return $this->laravel->databasePath()."/factories/{$name}.php"; + return 'App\Model'; } /** diff --git a/Console/Factories/stubs/factory.stub b/Console/Factories/stubs/factory.stub index bc895ed26..b85cdf4b4 100644 --- a/Console/Factories/stubs/factory.stub +++ b/Console/Factories/stubs/factory.stub @@ -1,9 +1,8 @@ $database, '--path' => $this->input->getOption('path'), '--realpath' => $this->input->getOption('realpath'), + '--schema-path' => $this->input->getOption('schema-path'), '--force' => true, '--step' => $this->option('step'), ])); @@ -98,6 +99,7 @@ protected function getOptions() ['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'], diff --git a/Console/Migrations/MigrateCommand.php b/Console/Migrations/MigrateCommand.php index f547d8d5e..e18835301 100755 --- a/Console/Migrations/MigrateCommand.php +++ b/Console/Migrations/MigrateCommand.php @@ -6,7 +6,6 @@ use Illuminate\Contracts\Events\Dispatcher; use Illuminate\Database\Events\SchemaLoaded; use Illuminate\Database\Migrations\Migrator; -use Illuminate\Database\SQLiteConnection; use Illuminate\Database\SqlServerConnection; class MigrateCommand extends BaseCommand @@ -127,8 +126,7 @@ protected function loadSchemaState() // 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 SQLiteConnection || - $connection instanceof SqlServerConnection || + if ($connection instanceof SqlServerConnection || ! is_file($path = $this->schemaPath($connection))) { return; } diff --git a/Eloquent/Builder.php b/Eloquent/Builder.php index f628ce788..77b41e0fa 100755 --- a/Eloquent/Builder.php +++ b/Eloquent/Builder.php @@ -790,7 +790,7 @@ public function forceCreate(array $attributes) } /** - * Update a record in the database. + * Update records in the database. * * @param array $values * @return int @@ -862,7 +862,7 @@ protected function addUpdatedAtColumn(array $values) } /** - * Delete a record from the database. + * Delete records from the database. * * @return mixed */ diff --git a/Eloquent/Concerns/HasAttributes.php b/Eloquent/Concerns/HasAttributes.php index 0fe957aeb..dc644e88d 100644 --- a/Eloquent/Concerns/HasAttributes.php +++ b/Eloquent/Concerns/HasAttributes.php @@ -15,6 +15,7 @@ use Illuminate\Support\Collection as BaseCollection; use Illuminate\Support\Facades\Date; use Illuminate\Support\Str; +use InvalidArgumentException; use LogicException; trait HasAttributes @@ -915,11 +916,13 @@ protected function asDateTime($value) // Finally, we will just assume this date is in the format used by default on // the database connection and use that format to create the Carbon object // that is returned back out to the developers after we convert it here. - if (Date::hasFormat($value, $format)) { - return Date::createFromFormat($format, $value); + try { + $date = Date::createFromFormat($format, $value); + } catch (InvalidArgumentException $e) { + $date = false; } - return Date::parse($value); + return $date ?: Date::parse($value); } /** diff --git a/Eloquent/Factories/Factory.php b/Eloquent/Factories/Factory.php index a284b19ac..ee0e74440 100644 --- a/Eloquent/Factories/Factory.php +++ b/Eloquent/Factories/Factory.php @@ -5,6 +5,7 @@ use Closure; use Faker\Generator; use Illuminate\Container\Container; +use Illuminate\Database\Eloquent\Collection as EloquentCollection; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Collection; use Illuminate\Support\Str; @@ -125,6 +126,7 @@ public function __construct($count = null, $this->afterMaking = $afterMaking ?: new Collection; $this->afterCreating = $afterCreating ?: new Collection; $this->connection = $connection; + $this->faker = $this->withFaker(); } /** @@ -166,6 +168,18 @@ public function configure() return $this; } + /** + * Get the raw attributes generated by the factory. + * + * @param array $attributes + * @param \Illuminate\Database\Eloquent\Model|null $parent + * @return array + */ + public function raw($attributes = [], ?Model $parent = null) + { + return $this->state($attributes)->getExpandedAttributes($parent); + } + /** * Create a single model and persist it to the database. * @@ -177,6 +191,21 @@ public function createOne($attributes = []) return $this->count(null)->create($attributes); } + /** + * Create a collection of models and persist them to the database. + * + * @param iterable $records + * @return \Illuminate\Database\Eloquent\Collection|mixed + */ + public function createMany(iterable $records) + { + return new EloquentCollection( + array_map(function ($record) { + return $this->state($record)->create(); + }, $records) + ); + } + /** * Create a collection of models and persist them to the database. * @@ -318,8 +347,6 @@ protected function getExpandedAttributes(?Model $parent) */ protected function getRawAttributes(?Model $parent) { - $this->faker = $this->withFaker(); - return $this->states->pipe(function ($states) { return $this->for->isEmpty() ? $states : new Collection(array_merge([function () { return $this->parentResolvers(); @@ -678,9 +705,13 @@ public function __call($method, $parameters) $relationship = Str::camel(Str::substr($method, 3)); - $factory = static::factoryForModel( - get_class($this->newModel()->{$relationship}()->getRelated()) - ); + $relatedModel = get_class($this->newModel()->{$relationship}()->getRelated()); + + if (method_exists($relatedModel, 'newFactory')) { + $factory = $relatedModel::newFactory() ?: static::factoryForModel($relatedModel); + } else { + $factory = static::factoryForModel($relatedModel); + } if (Str::startsWith($method, 'for')) { return $this->for($factory->state($parameters[0] ?? []), $relationship); diff --git a/Eloquent/Factories/HasFactory.php b/Eloquent/Factories/HasFactory.php index ad3063e47..383899abb 100644 --- a/Eloquent/Factories/HasFactory.php +++ b/Eloquent/Factories/HasFactory.php @@ -13,8 +13,20 @@ trait HasFactory */ public static function factory($count = null, $state = []) { - return Factory::factoryForModel(get_called_class()) + $factory = static::newFactory() ?: Factory::factoryForModel(get_called_class()); + + return $factory ->count(is_numeric($count) ? $count : null) ->state(is_callable($count) || is_array($count) ? $count : $state); } + + /** + * Create a new factory instance for the model. + * + * @return \Illuminate\Database\Eloquent\Factories\Factory + */ + protected static function newFactory() + { + // + } } diff --git a/Eloquent/Relations/MorphTo.php b/Eloquent/Relations/MorphTo.php index 22d1d4d2c..891c172df 100644 --- a/Eloquent/Relations/MorphTo.php +++ b/Eloquent/Relations/MorphTo.php @@ -136,7 +136,7 @@ protected function getResultsByType($type) $whereIn = $this->whereInMethod($instance, $ownerKey); return $query->{$whereIn}( - $instance->getTable().'.'.$ownerKey, $this->gatherKeysByType($type) + $instance->getTable().'.'.$ownerKey, $this->gatherKeysByType($type, $instance->getKeyType()) )->get(); } @@ -144,11 +144,16 @@ protected function getResultsByType($type) * Gather all of the foreign keys for a given type. * * @param string $type + * @param string $keyType * @return array */ - protected function gatherKeysByType($type) + protected function gatherKeysByType($type, $keyType) { - return array_keys($this->dictionary[$type]); + return $keyType !== 'string' + ? array_keys($this->dictionary[$type]) + : array_map(function ($modelId) { + return (string) $modelId; + }, array_keys($this->dictionary[$type])); } /** diff --git a/Query/Builder.php b/Query/Builder.php index 750152dc3..c1ce16150 100755 --- a/Query/Builder.php +++ b/Query/Builder.php @@ -618,6 +618,26 @@ public function crossJoin($table, $first = null, $operator = null, $second = nul return $this; } + /** + * Add a subquery cross join to the query. + * + * @param \Closure|\Illuminate\Database\Query\Builder|string $query + * @param string $as + * @return $this + */ + public function crossJoinSub($query, $as) + { + [$query, $bindings] = $this->createSub($query); + + $expression = '('.$query.') as '.$this->grammar->wrapTable($as); + + $this->addBinding($bindings, 'join'); + + $this->joins[] = $this->newJoinClause($this, 'cross', new Expression($expression)); + + return $this; + } + /** * Get a new join clause. * @@ -1088,7 +1108,7 @@ public function whereNotNull($columns, $boolean = 'and') /** * Add a where between statement to the query. * - * @param string $column + * @param string|\Illuminate\Database\Query\Expression $column * @param array $values * @param string $boolean * @param bool $not @@ -2770,7 +2790,7 @@ protected function onceWithColumns($columns, $callback) } /** - * Insert a new record into the database. + * Insert new records into the database. * * @param array $values * @return bool @@ -2809,7 +2829,7 @@ public function insert(array $values) } /** - * Insert a new record into the database while ignoring errors. + * Insert new records into the database while ignoring errors. * * @param array $values * @return int @@ -2869,7 +2889,7 @@ public function insertUsing(array $columns, $query) } /** - * Update a record in the database. + * Update records in the database. * * @param array $values * @return int @@ -2950,7 +2970,7 @@ public function decrement($column, $amount = 1, array $extra = []) } /** - * Delete a record from the database. + * Delete records from the database. * * @param mixed $id * @return int diff --git a/SQLiteConnection.php b/SQLiteConnection.php index 06d7fbf73..e647b13a6 100755 --- a/SQLiteConnection.php +++ b/SQLiteConnection.php @@ -7,8 +7,8 @@ use Illuminate\Database\Query\Processors\SQLiteProcessor; use Illuminate\Database\Schema\Grammars\SQLiteGrammar as SchemaGrammar; use Illuminate\Database\Schema\SQLiteBuilder; +use Illuminate\Database\Schema\SqliteSchemaState; use Illuminate\Filesystem\Filesystem; -use RuntimeException; class SQLiteConnection extends Connection { @@ -80,7 +80,7 @@ protected function getDefaultSchemaGrammar() */ public function getSchemaState(Filesystem $files = null, callable $processFactory = null) { - throw new RuntimeException('Schema dumping is not supported when using SQLite.'); + return new SqliteSchemaState($this, $files, $processFactory); } /** diff --git a/Schema/MySqlSchemaState.php b/Schema/MySqlSchemaState.php index c3bb01ecb..3cb7dc93b 100644 --- a/Schema/MySqlSchemaState.php +++ b/Schema/MySqlSchemaState.php @@ -17,7 +17,7 @@ class MySqlSchemaState extends SchemaState public function dump($path) { $this->executeDumpProcess($this->makeProcess( - $this->baseDumpCommand().' --routines --result-file=$LARAVEL_LOAD_PATH --no-data' + $this->baseDumpCommand().' --routines --result-file="${:LARAVEL_LOAD_PATH}" --no-data' ), $this->output, array_merge($this->baseVariables($this->connection->getConfig()), [ 'LARAVEL_LOAD_PATH' => $path, ])); @@ -67,7 +67,7 @@ protected function appendMigrationData(string $path) */ public function load($path) { - $process = $this->makeProcess('mysql --host=$LARAVEL_LOAD_HOST --port=$LARAVEL_LOAD_PORT --user=$LARAVEL_LOAD_USER --password=$LARAVEL_LOAD_PASSWORD --database=$LARAVEL_LOAD_DATABASE < $LARAVEL_LOAD_PATH'); + $process = $this->makeProcess('mysql --host="${:LARAVEL_LOAD_HOST}" --port="${:LARAVEL_LOAD_PORT}" --user="${:LARAVEL_LOAD_USER}" --password="${:LARAVEL_LOAD_PASSWORD}" --database="${:LARAVEL_LOAD_DATABASE}" < "${:LARAVEL_LOAD_PATH}"'); $process->mustRun(null, array_merge($this->baseVariables($this->connection->getConfig()), [ 'LARAVEL_LOAD_PATH' => $path, @@ -81,9 +81,11 @@ public function load($path) */ protected function baseDumpCommand() { + $columnStatistics = $this->connection->isMaria() ? '' : '--column-statistics=0'; + $gtidPurged = $this->connection->isMaria() ? '' : '--set-gtid-purged=OFF'; - return 'mysqldump '.$gtidPurged.' --column-statistics=0 --skip-add-drop-table --skip-add-locks --skip-comments --skip-set-charset --tz-utc --host=$LARAVEL_LOAD_HOST --port=$LARAVEL_LOAD_PORT --user=$LARAVEL_LOAD_USER --password=$LARAVEL_LOAD_PASSWORD $LARAVEL_LOAD_DATABASE'; + return 'mysqldump '.$gtidPurged.' '.$columnStatistics.' --skip-add-drop-table --skip-add-locks --skip-comments --skip-set-charset --tz-utc --host="${:LARAVEL_LOAD_HOST}" --port="${:LARAVEL_LOAD_PORT}" --user="${:LARAVEL_LOAD_USER}" --password="${:LARAVEL_LOAD_PASSWORD}" "${:LARAVEL_LOAD_DATABASE}"'; } /** @@ -95,7 +97,7 @@ protected function baseDumpCommand() protected function baseVariables(array $config) { return [ - 'LARAVEL_LOAD_HOST' => $config['host'], + 'LARAVEL_LOAD_HOST' => is_array($config['host']) ? $config['host'][0] : $config['host'], 'LARAVEL_LOAD_PORT' => $config['port'], 'LARAVEL_LOAD_USER' => $config['username'], 'LARAVEL_LOAD_PASSWORD' => $config['password'], @@ -116,7 +118,7 @@ protected function executeDumpProcess(Process $process, $output, array $variable try { $process->mustRun($output, $variables); } catch (Exception $e) { - if (Str::contains($e->getMessage(), 'column_statistics')) { + if (Str::contains($e->getMessage(), ['column-statistics', 'column_statistics'])) { return $this->executeDumpProcess(Process::fromShellCommandLine( str_replace(' --column-statistics=0', '', $process->getCommandLine()) ), $output, $variables); diff --git a/Schema/PostgresSchemaState.php b/Schema/PostgresSchemaState.php index d7f1d46ae..81fef2632 100644 --- a/Schema/PostgresSchemaState.php +++ b/Schema/PostgresSchemaState.php @@ -59,7 +59,7 @@ public function load($path) } /** - * Get the base dump command arguments for MySQL as a string. + * Get the base dump command arguments for PostgreSQL as a string. * * @return string */ diff --git a/Schema/SqliteSchemaState.php b/Schema/SqliteSchemaState.php new file mode 100644 index 000000000..773affb2c --- /dev/null +++ b/Schema/SqliteSchemaState.php @@ -0,0 +1,90 @@ +makeProcess( + $this->baseCommand().' .schema' + ))->mustRun(null, array_merge($this->baseVariables($this->connection->getConfig()), [ + // + ])); + + $migrations = collect(preg_split("/\r\n|\n|\r/", $process->getOutput()))->filter(function ($line) { + return stripos($line, 'sqlite_sequence') === false && + strlen($line) > 0; + })->all(); + + $this->files->put($path, implode(PHP_EOL, $migrations).PHP_EOL); + + $this->appendMigrationData($path); + } + + /** + * Append the migration data to the schema dump. + * + * @return void + */ + protected function appendMigrationData(string $path) + { + with($process = $this->makeProcess( + $this->baseCommand().' ".dump \'migrations\'"' + ))->mustRun(null, array_merge($this->baseVariables($this->connection->getConfig()), [ + // + ])); + + $migrations = collect(preg_split("/\r\n|\n|\r/", $process->getOutput()))->filter(function ($line) { + return preg_match('/^\s*(--|INSERT\s)/iu', $line) === 1 && + strlen($line) > 0; + })->all(); + + $this->files->append($path, implode(PHP_EOL, $migrations).PHP_EOL); + } + + /** + * Load the given schema file into the database. + * + * @param string $path + * + * @return void + */ + public function load($path) + { + $process = $this->makeProcess($this->baseCommand().' < $LARAVEL_LOAD_PATH'); + + $process->mustRun(null, array_merge($this->baseVariables($this->connection->getConfig()), [ + 'LARAVEL_LOAD_PATH' => $path, + ])); + } + + /** + * Get the base sqlite command arguments as a string. + * + * @return string + */ + protected function baseCommand() + { + return 'sqlite3 $LARAVEL_LOAD_DATABASE'; + } + + /** + * Get the base variables for a dump / load command. + * + * @return array + */ + protected function baseVariables(array $config) + { + return [ + 'LARAVEL_LOAD_DATABASE' => $config['database'], + ]; + } +} diff --git a/Seeder.php b/Seeder.php index 01f06ba61..441fa27d6 100755 --- a/Seeder.php +++ b/Seeder.php @@ -24,14 +24,14 @@ abstract class Seeder protected $command; /** - * Seed the given connection from the given path. + * Run the given seeder class. * * @param array|string $class * @param bool $silent - * @param mixed ...$parameters + * @param array $parameters * @return $this */ - public function call($class, $silent = false, ...$parameters) + public function call($class, $silent = false, array $parameters = []) { $classes = Arr::wrap($class); @@ -46,7 +46,7 @@ public function call($class, $silent = false, ...$parameters) $startTime = microtime(true); - $seeder->__invoke(...$parameters); + $seeder->__invoke($parameters); $runTime = number_format((microtime(true) - $startTime) * 1000, 2); @@ -59,15 +59,27 @@ public function call($class, $silent = false, ...$parameters) } /** - * Silently seed the given connection from the given path. + * Run the given seeder class. * * @param array|string $class - * @param mixed ...$parameters + * @param array $parameters * @return void */ - public function callSilent($class, ...$parameters) + public function callWith($class, array $parameters = []) { - $this->call($class, true, ...$parameters); + $this->call($class, false, $parameters); + } + + /** + * Silently run the given seeder class. + * + * @param array|string $class + * @param array $parameters + * @return void + */ + public function callSilent($class, array $parameters = []) + { + $this->call($class, true, $parameters); } /** @@ -122,12 +134,12 @@ public function setCommand(Command $command) /** * Run the database seeds. * - * @param mixed ...$parameters + * @param array $parameters * @return mixed * * @throws \InvalidArgumentException */ - public function __invoke(...$parameters) + public function __invoke(array $parameters = []) { if (! method_exists($this, 'run')) { throw new InvalidArgumentException('Method [run] missing from '.get_class($this));