Skip to content

Commit

Permalink
support Laravel 11
Browse files Browse the repository at this point in the history
  • Loading branch information
guidocella committed Mar 14, 2024
1 parent afd09d7 commit 704a2bb
Show file tree
Hide file tree
Showing 8 changed files with 218 additions and 117 deletions.
10 changes: 4 additions & 6 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,13 @@
"keywords": ["laravel", "eloquent", "database", "faker"],
"license": "MIT",
"require": {
"php": ">=8.1",
"illuminate/database": "^10",
"fakerphp/faker": "^1",
"doctrine/dbal": "^3"
"illuminate/database": "^11",
"fakerphp/faker": "^1"
},
"require-dev": {
"guidocella/laravel-multilingual": "*",
"laravel/laravel": "^10",
"phpunit/phpunit": "^10"
"laravel/laravel": "^11",
"phpunit/phpunit": "^11"
},
"autoload": {
"psr-4": {
Expand Down
65 changes: 38 additions & 27 deletions src/ColumnTypeGuesser.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
namespace GuidoCella\EloquentPopulator;

use Closure;
use Doctrine\DBAL\Schema\Column;
use Faker\Generator;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;

class ColumnTypeGuesser
{
Expand All @@ -15,24 +16,32 @@ public function __construct(Generator $generator)
$this->generator = $generator;
}

public function guessFormat(Column $column, string $tableName): ?Closure
public function guessFormat(array $column): ?Closure
{
switch ($column->getType()->getName()) {
switch ($column['type_name']) {
case 'smallint':
return fn () => rand(0, 65535);
return fn () => rand(0, 32767);

case 'int':
case 'integer':
return fn () => rand(0, 2147483647);

case 'bigint':
return fn () => rand(0, intval('18446744073709551615'));
return fn () => rand(0, 9223372036854775807);

case 'float':
return fn () => rand(0, 4294967295) / rand(1, 4294967295);
case 'double':
return fn () => $this->generator->randomFloat();

case 'decimal':
$maxDigits = $column->getPrecision();
$maxDecimalDigits = $column->getScale();
case 'numeric':
// The precision is unavailable with SQLite.
if ($column['type_name'] === 'numeric') {
$maxDigits = 3;
$maxDecimalDigits = 1;
} else {
[$maxDigits, $maxDecimalDigits] = explode(',', Str::before(Str::after($column['type'], '('), ')'));
}

$max = 10 ** ($maxDigits - $maxDecimalDigits);

Expand All @@ -47,8 +56,12 @@ public function guessFormat(Column $column, string $tableName): ?Closure
return $value;
};

case 'string':
$size = $column->getLength() ?: 60;
case 'varchar':
case 'char':
$size = Str::before(Str::after($column['type'], '('), ')');
if ($size === 'varchar') { // SQLite
$size = 5;
}

// If Faker's text() $maxNbChars argument is greater than 99,
// the text it generates can have new lines which are ignored by non-textarea inputs
Expand All @@ -57,46 +70,44 @@ public function guessFormat(Column $column, string $tableName): ?Closure
$size = 99;
}

return function () use ($size, $column, $tableName) {
return function () use ($size) {
if ($size >= 5) {
return $this->generator->text($size);
}

$columnName = "$tableName.{$column->getName()}";

throw new \InvalidArgumentException(
"$columnName is a string shorter than 5 characters,"
." but Faker's text() can only generate text of at least 5 characters.".PHP_EOL
."Please specify a more accurate formatter for $columnName."
);

// Of course we could just use str_random($size) here,
// but for the CHAR columns for which I got this error
// I found that it was better to specify a more precise formatter anyway,
// e.g. $faker->countryCode for sender_country.
return Str::random($size);
};

case 'text':
return fn () => $this->generator->text();

case 'guid':
case 'uuid':
return fn () => $this->generator->uuid();

case 'date':
case 'datetime':
case 'datetimetz':
case 'timestamp':
return fn () => $this->generator->datetime();

case 'time':
return fn () => $this->generator->time();

case 'boolean':
return fn () => $this->generator->boolean();
// Unfortunately Doctrine maps all TINYINT to BooleanType.
case 'tinyint':
if ($column['type'] === 'tinyint(1)') {
return fn () => $this->generator->boolean();
}

return fn () => rand(0, 127);

case 'json':
case 'json_array':
case 'longtext': // MariaDB
return fn () => json_encode([$this->generator->word() => $this->generator->word()]);

case 'enum':
return fn () => Arr::random(explode(',', str_replace("'", '', substr($column['type'], 5, -1))));

default:
return null;
}
Expand Down
86 changes: 14 additions & 72 deletions src/ModelPopulator.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,82 +3,29 @@
namespace GuidoCella\EloquentPopulator;

use Faker\Generator;
use Faker\Guesser\Name;
use GuidoCella\Multilingual\Translatable;
use Illuminate\Container\Container;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;

class ModelPopulator
{
protected Model $model;

protected Generator $generator;

/**
* @var \Doctrine\DBAL\Schema\Column[]
*/
protected array $columns = [];

public function __construct(string $modelClass)
{
$this->model = new $modelClass();
$this->generator = Container::getInstance()->make(Generator::class);
$this->setColumns();
}

protected function setColumns(): void
{
$schema = $this->model->getConnection()->getDoctrineSchemaManager();
$platform = $schema->getDatabasePlatform();

// Prevent a DBALException if the table contains an enum.
$platform->registerDoctrineTypeMapping('enum', 'string');

[$table, $database] = $this->getTableAndDatabase();

$this->columns = $this->model->getConnection()->getDoctrineConnection()->fetchAllAssociative(
$platform->getListTableColumnsSQL($table, $database)
$this->columns = array_filter(
$this->model->getConnection()->getSchemaBuilder()->getColumns($this->model->getTable()),
fn ($column) => $column['generation'] === null, // filter out virtual columns
);

$this->rejectVirtualColumns();

$columns = $this->columns;
$this->columns = (fn () => $this->_getPortableTableColumnList($table, $database, $columns))->call($schema);

$this->unquoteColumnNames($platform->getIdentifierQuoteCharacter());
}

protected function getTableAndDatabase(): array
{
$table = $this->model->getConnection()->getTablePrefix().$this->model->getTable();

if (strpos($table, '.')) {
[$database, $table] = explode('.', $table);
}

return [$table, $database ?? null];
}

// For MySql and MariaDB
protected function rejectVirtualColumns()
{
$this->columns = array_filter($this->columns, fn ($column) => !isset($column['Extra']) || !Str::contains($column['Extra'], 'VIRTUAL'));
}

/**
* Unquote column names that have been quoted by Doctrine because they are reserved keywords.
*/
protected function unquoteColumnNames(string $quoteCharacter): void
{
foreach ($this->columns as $columnName => $columnData) {
if (Str::startsWith($columnName, $quoteCharacter)) {
$this->columns[substr($columnName, 1, -1)] = Arr::pull($this->columns, $columnName);
}
}
}

public function guessFormatters(): array
Expand All @@ -88,41 +35,35 @@ public function guessFormatters(): array
$nameGuesser = new Name($this->generator);
$columnTypeGuesser = new ColumnTypeGuesser($this->generator);

foreach ($this->columns as $columnName => $column) {
if ($columnName === $this->model->getKeyName() && $column->getAutoincrement()) {
foreach ($this->columns as $column) {
if ($column['name'] === $this->model->getKeyName() && $column['auto_increment']) {
continue;
}

if (
method_exists($this->model, 'getDeletedAtColumn')
&& $columnName === $this->model->getDeletedAtColumn()
&& $column['name'] === $this->model->getDeletedAtColumn()
) {
continue;
}

if (
!Populator::$seeding
&& in_array($columnName, [$this->model->getCreatedAtColumn(), $this->model->getUpdatedAtColumn()])
&& in_array($column['name'], [$this->model->getCreatedAtColumn(), $this->model->getUpdatedAtColumn()])
) {
continue;
}

$formatter = $nameGuesser->guessFormat(
$columnName,
$column->getLength()
) ?? $columnTypeGuesser->guessFormat(
$column,
$this->model->getTable()
);
$formatter = $nameGuesser->guessFormat($column) ?? $columnTypeGuesser->guessFormat($column);

if (!$formatter) {
continue;
}

if ($column->getNotnull() || !Populator::$seeding) {
$formatters[$columnName] = $formatter;
if (!$column['nullable'] || !Populator::$seeding) {
$formatters[$column['name']] = $formatter;
} else {
$formatters[$columnName] = fn () => rand(0, 1) ? $formatter() : null;
$formatters[$column['name']] = fn () => rand(0, 1) ? $formatter() : null;
}
}

Expand Down Expand Up @@ -190,11 +131,12 @@ protected function associateBelongsTo(array &$formatters, BelongsTo $relation):
{
$relatedClass = get_class($relation->getRelated());
$foreignKey = $relation->getForeignKeyName();
$foreignKeyColumn = Arr::first($this->columns, fn ($column) => $column['name'] === $foreignKey);

// Ignore dynamic relations in which the foreign key is selected with a subquery.
// https://reinink.ca/articles/dynamic-relationships-in-laravel-using-subqueries
// (superseded by Has One Of Many relationships)
if (!isset($this->columns[$foreignKey])) {
if (!$foreignKeyColumn) {
return;
}

Expand All @@ -214,7 +156,7 @@ protected function associateBelongsTo(array &$formatters, BelongsTo $relation):

$relatedFactory = $relatedFactoryClass::new();

if ($this->columns[$foreignKey]->getNotnull() || !Populator::$seeding) {
if (!$foreignKeyColumn['nullable'] || !Populator::$seeding) {
$formatters[$foreignKey] = $relatedFactory;
} else {
$formatters[$foreignKey] = fn () => rand(0, 1) ? $relatedFactory : null;
Expand Down
Loading

0 comments on commit 704a2bb

Please sign in to comment.