Merge pull request #10972 from filamentphp/laravel-11

Laravel 11
This commit is contained in:
Dan Harrin
2024-02-27 15:30:01 +00:00
committed by GitHub
38 changed files with 823 additions and 1449 deletions
+1
View File
@@ -3,4 +3,5 @@
/.github export-ignore
/.gitignore export-ignore
/phpunit.xml.dist export-ignore
/testbench.yaml export-ignore
/tests export-ignore
+6 -1
View File
@@ -11,11 +11,16 @@ jobs:
fail-fast: false
matrix:
php: [8.3, 8.2, 8.1]
laravel: [10.*]
laravel: [11.*, 10.*]
dependency-version: [prefer-stable]
include:
- laravel: 11.*
testbench: 9.*
- laravel: 10.*
testbench: 8.*
exclude:
- laravel: 11.*
php: 8.1
name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.dependency-version }}
steps:
- uses: actions/checkout@v3
+6 -1
View File
@@ -11,11 +11,16 @@ jobs:
fail-fast: false
matrix:
php: [8.3, 8.2, 8.1]
laravel: [10.*]
laravel: [11.*, 10.*]
dependency-version: [prefer-stable]
include:
- laravel: 11.*
testbench: 9.*
- laravel: 10.*
testbench: 8.*
exclude:
- laravel: 11.*
php: 8.1
name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.dependency-version }}
steps:
- uses: actions/checkout@v3
+3 -6
View File
@@ -10,22 +10,19 @@
"require-dev": {
"anourvalar/eloquent-serialize": "^1.2",
"danharrin/livewire-rate-limiting": "^0.3|^1.0",
"doctrine/dbal": "^3.2",
"filament/support": "*",
"kirschbaum-development/eloquent-power-joins": "^3.0",
"larastan/larastan": "^2.2",
"laravel/cashier": "^14.2",
"laravel/pint": "^1.0",
"league/csv": "^9.14",
"league/flysystem-aws-s3-v3": "^3.0",
"nunomaduro/termwind": "^1.0",
"nunomaduro/termwind": "^1.0|^2.0",
"openspout/openspout": "^4.23",
"orchestra/testbench": "^8.0",
"orchestra/testbench": "^8.20|^9.0",
"pestphp/pest": "^2.0",
"pestphp/pest-plugin-laravel": "^2.0",
"phpstan/extension-installer": "^1.1",
"phpstan/phpstan": "v1.10.54",
"rector/rector": "^0.17",
"phpstan/phpstan": "^1.10",
"spatie/laravel-medialibrary": "^10.0|^11.0",
"spatie/laravel-ray": "^1.29",
"spatie/laravel-tags": "^4.2",
Generated
+486 -1162
View File
File diff suppressed because it is too large Load Diff
+3 -3
View File
@@ -14,9 +14,9 @@
"filament/infolists": "self.version",
"filament/notifications": "self.version",
"filament/support": "self.version",
"illuminate/contracts": "^10.45",
"illuminate/database": "^10.45",
"illuminate/support": "^10.45",
"illuminate/contracts": "^10.45|^11.0",
"illuminate/database": "^10.45|^11.0",
"illuminate/support": "^10.45|^11.0",
"league/csv": "^9.14",
"openspout/openspout": "^4.23",
"spatie/laravel-package-tools": "^1.9"
@@ -67,8 +67,6 @@ If you'd like to save time, Filament can automatically generate the [columns](#d
php artisan make:filament-importer Product --generate
```
> If your table contains ENUM columns, the `doctrine/dbal` package we use is unable to scan your table and will crash. Hence, Filament is unable to generate the columns for your importer if it contains an ENUM column. Read more about this issue [here](https://github.com/doctrine/dbal/issues/3819#issuecomment-573419808).
## Defining importer columns
To define the columns that can be imported, you need to override the `getColumns()` method on your importer class, returning an array of `ImportColumn` objects:
@@ -82,8 +82,6 @@ If you'd like to save time, Filament can automatically generate the [columns](#d
php artisan make:filament-exporter Product --generate
```
> If your table contains ENUM columns, the `doctrine/dbal` package we use is unable to scan your table and will crash. Hence, Filament is unable to generate the columns for your exporter if it contains an ENUM column. Read more about this issue [here](https://github.com/doctrine/dbal/issues/3819#issuecomment-573419808).
## Defining exporter columns
To define the columns that can be exported, you need to override the `getColumns()` method on your exporter class, returning an array of `ExportColumn` objects:
@@ -14,17 +14,12 @@ trait CanGenerateExporterColumns
return '//';
}
$schema = $this->getModelSchema($model);
$table = $this->getModelTable($model);
if (blank($table)) {
return '//';
}
$columns = [];
foreach ($table->getColumns() as $column) {
$columnName = $column->getName();
foreach ($schema->getColumnListing($table) as $columnName) {
if (str($columnName)->endsWith([
'_token',
])) {
@@ -38,10 +33,10 @@ trait CanGenerateExporterColumns
}
if (str($columnName)->endsWith('_id')) {
$guessedRelationshipName = $this->guessBelongsToRelationshipName($column, $model);
$guessedRelationshipName = $this->guessBelongsToRelationshipName($columnName, $model);
if (filled($guessedRelationshipName)) {
$guessedRelationshipTitleColumnName = $this->guessBelongsToRelationshipTitleColumnName($column, app($model)->{$guessedRelationshipName}()->getModel()::class);
$guessedRelationshipTitleColumnName = $this->guessBelongsToRelationshipTitleColumnName($columnName, app($model)->{$guessedRelationshipName}()->getModel()::class);
$columnName = "{$guessedRelationshipName}.{$guessedRelationshipTitleColumnName}";
}
@@ -2,7 +2,6 @@
namespace Filament\Actions\Commands\Concerns;
use Doctrine\DBAL\Types;
use Illuminate\Support\Str;
trait CanGenerateImporterColumns
@@ -15,20 +14,17 @@ trait CanGenerateImporterColumns
return '//';
}
$schema = $this->getModelSchema($model);
$table = $this->getModelTable($model);
if (blank($table)) {
return '//';
}
$columns = [];
foreach ($table->getColumns() as $column) {
if ($column->getAutoincrement()) {
foreach ($schema->getColumns($table) as $column) {
if ($column['auto_increment']) {
continue;
}
$columnName = $column->getName();
$columnName = $column['name'];
if (str($columnName)->is([
app($model)->getKeyName(),
@@ -50,7 +46,7 @@ trait CanGenerateImporterColumns
$columnData['label'] = [Str::upper($columnName)];
}
if ($column->getNotnull()) {
if (! $column['nullable']) {
$columnData['rules'][0][] = 'required';
$columnData['requiredMapping'] = [];
}
@@ -59,44 +55,39 @@ trait CanGenerateImporterColumns
$columnData['rules'][0][] = 'email';
}
$type = $this->parseColumnType($column);
if (
str($columnName)->endsWith('_id') &&
filled($guessedRelationshipName = $this->guessBelongsToRelationshipName($column, $model))
filled($guessedRelationshipName = $this->guessBelongsToRelationshipName($columnName, $model))
) {
$columnName = $guessedRelationshipName;
$columnData['relationship'] = [];
} elseif (in_array($column->getType()::class, [
Types\BooleanType::class,
} elseif (in_array($type['name'], [
'boolean',
])) {
$columnData['rules'][0][] = 'boolean';
$columnData['boolean'] = [];
} elseif (in_array($column->getType()::class, [
Types\DateImmutableType::class,
Types\DateType::class,
} elseif (in_array($type['name'], [
'date',
])) {
$columnData['rules'][0][] = 'date';
} elseif (in_array($column->getType()::class, [
Types\DateTimeImmutableType::class,
Types\DateTimeType::class,
Types\DateTimeTzImmutableType::class,
Types\DateTimeTzType::class,
} elseif (in_array($type['name'], [
'datetime',
'timestamp',
])) {
$columnData['rules'][0][] = 'datetime';
} elseif (in_array($column->getType()::class, [
Types\IntegerType::class,
Types\SmallIntType::class,
Types\BigIntType::class,
} elseif (in_array($type['name'], [
'integer',
'decimal',
'float',
'double',
'money',
])) {
$columnData['rules'][0][] = 'integer';
$columnData['numeric'] = [];
} elseif (in_array($column->getType()::class, [
Types\DecimalType::class,
Types\FloatType::class,
])) {
$columnData['rules'][0][] = 'integer';
$columnData['numeric'] = [];
} elseif ($length = $column->getLength()) {
$columnData['rules'][0][] = "max:{$length}";
} elseif (isset($type['length'])) {
$columnData['rules'][0][] = "max:{$type['length']}";
}
// Move rules to the end of the column definition.
+7 -7
View File
@@ -12,13 +12,13 @@
"danharrin/date-format-converter": "^0.3",
"filament/actions": "self.version",
"filament/support": "self.version",
"illuminate/console": "^10.45",
"illuminate/contracts": "^10.45",
"illuminate/database": "^10.45",
"illuminate/filesystem": "^10.45",
"illuminate/support": "^10.45",
"illuminate/validation": "^10.45",
"illuminate/view": "^10.45",
"illuminate/console": "^10.45|^11.0",
"illuminate/contracts": "^10.45|^11.0",
"illuminate/database": "^10.45|^11.0",
"illuminate/filesystem": "^10.45|^11.0",
"illuminate/support": "^10.45|^11.0",
"illuminate/validation": "^10.45|^11.0",
"illuminate/view": "^10.45|^11.0",
"spatie/laravel-package-tools": "^1.9"
},
"autoload": {
@@ -329,5 +329,3 @@ Filament is also able to guess which form fields you want in the schema, based o
```bash
php artisan make:livewire-form Products/CreateProduct --generate
```
> If your table contains ENUM columns, the `doctrine/dbal` package we use is unable to scan your table and will crash. Hence, Filament is unable to generate the schema for your table if it contains an ENUM column. Read more about this issue [here](https://github.com/doctrine/dbal/issues/3819#issuecomment-573419808).
@@ -2,7 +2,6 @@
namespace Filament\Forms\Commands\Concerns;
use Doctrine\DBAL\Types;
use Filament\Forms;
use Illuminate\Support\Str;
@@ -16,20 +15,17 @@ trait CanGenerateForms
return '//';
}
$schema = $this->getModelSchema($model);
$table = $this->getModelTable($model);
if (blank($table)) {
return '//';
}
$components = [];
foreach ($table->getColumns() as $column) {
if ($column->getAutoincrement()) {
foreach ($schema->getColumns($table) as $column) {
if ($column['auto_increment']) {
continue;
}
$columnName = $column->getName();
$columnName = $column['name'];
if (str($columnName)->is([
app($model)->getKeyName(),
@@ -41,22 +37,24 @@ trait CanGenerateForms
continue;
}
$type = $this->parseColumnType($column);
$componentData = [];
$componentData['type'] = match (true) {
$column->getType()::class === Types\BooleanType::class => Forms\Components\Toggle::class,
in_array($column->getType()::class, [Types\DateImmutableType::class, Types\DateType::class]) => Forms\Components\DatePicker::class,
in_array($column->getType()::class, [Types\DateTimeImmutableType::class, Types\DateTimeType::class, Types\DateTimeTzImmutableType::class, Types\DateTimeTzType::class]) => Forms\Components\DateTimePicker::class,
$column->getType()::class === Types\TextType::class => Forms\Components\Textarea::class,
$type['name'] === 'boolean' => Forms\Components\Toggle::class,
$type['name'] === 'date' => Forms\Components\DatePicker::class,
in_array($type['name'], ['datetime', 'timestamp']) => Forms\Components\DateTimePicker::class,
$type['name'] === 'text' => Forms\Components\Textarea::class,
$columnName === 'image', str($columnName)->startsWith('image_'), str($columnName)->contains('_image_'), str($columnName)->endsWith('_image') => Forms\Components\FileUpload::class,
default => Forms\Components\TextInput::class,
};
if (str($columnName)->endsWith('_id')) {
$guessedRelationshipName = $this->guessBelongsToRelationshipName($column, $model);
$guessedRelationshipName = $this->guessBelongsToRelationshipName($columnName, $model);
if (filled($guessedRelationshipName)) {
$guessedRelationshipTitleColumnName = $this->guessBelongsToRelationshipTitleColumnName($column, app($model)->{$guessedRelationshipName}()->getModel()::class);
$guessedRelationshipTitleColumnName = $this->guessBelongsToRelationshipTitleColumnName($columnName, app($model)->{$guessedRelationshipName}()->getModel()::class);
$componentData['type'] = Forms\Components\Select::class;
$componentData['relationship'] = [$guessedRelationshipName, $guessedRelationshipTitleColumnName];
@@ -89,40 +87,40 @@ trait CanGenerateForms
$componentData['image'] = [];
}
if ($column->getNotnull()) {
if (! $column['nullable']) {
$componentData['required'] = [];
}
if (in_array($column->getType()::class, [
Types\BigIntType::class,
Types\DecimalType::class,
Types\FloatType::class,
Types\IntegerType::class,
Types\SmallIntType::class,
if (in_array($type['name'], [
'integer',
'decimal',
'float',
'double',
'money',
])) {
if ($componentData['type'] === Forms\Components\TextInput::class) {
$componentData['numeric'] = [];
}
if (filled($column->getDefault())) {
$componentData['default'] = [$column->getDefault()];
if (filled($column['default'])) {
$componentData['default'] = [$this->parseDefaultExpression($column, $model)];
}
if (in_array($columnName, [
'cost',
'money',
'price',
])) {
]) || $type['name'] === 'money') {
$componentData['prefix'] = ['$'];
}
} elseif (in_array($componentData['type'], [
Forms\Components\TextInput::class,
Forms\Components\Textarea::class,
]) && ($length = $column->getLength())) {
$componentData['maxLength'] = [$length];
]) && isset($type['length'])) {
$componentData['maxLength'] = [$type['length']];
if (filled($column->getDefault())) {
$componentData['default'] = [$column->getDefault()];
if (filled($column['default'])) {
$componentData['default'] = [$this->parseDefaultExpression($column, $model)];
}
}
+6 -6
View File
@@ -11,12 +11,12 @@
"php": "^8.1",
"filament/actions": "self.version",
"filament/support": "self.version",
"illuminate/console": "^10.45",
"illuminate/contracts": "^10.45",
"illuminate/database": "^10.45",
"illuminate/filesystem": "^10.45",
"illuminate/support": "^10.45",
"illuminate/view": "^10.45",
"illuminate/console": "^10.45|^11.0",
"illuminate/contracts": "^10.45|^11.0",
"illuminate/database": "^10.45|^11.0",
"illuminate/filesystem": "^10.45|^11.0",
"illuminate/support": "^10.45|^11.0",
"illuminate/view": "^10.45|^11.0",
"spatie/laravel-package-tools": "^1.9"
},
"autoload": {
+4 -4
View File
@@ -11,10 +11,10 @@
"php": "^8.1",
"filament/actions": "self.version",
"filament/support": "self.version",
"illuminate/contracts": "^10.45",
"illuminate/filesystem": "^10.45",
"illuminate/notifications": "^10.45",
"illuminate/support": "^10.45",
"illuminate/contracts": "^10.45|^11.0",
"illuminate/filesystem": "^10.45|^11.0",
"illuminate/notifications": "^10.45|^11.0",
"illuminate/support": "^10.45|^11.0",
"spatie/laravel-package-tools": "^1.9"
},
"autoload": {
+10 -10
View File
@@ -17,16 +17,16 @@
"filament/support": "self.version",
"filament/tables": "self.version",
"filament/widgets": "self.version",
"illuminate/auth": "^10.45",
"illuminate/console": "^10.45",
"illuminate/contracts": "^10.45",
"illuminate/cookie": "^10.45",
"illuminate/database": "^10.45",
"illuminate/http": "^10.45",
"illuminate/routing": "^10.45",
"illuminate/session": "^10.45",
"illuminate/support": "^10.45",
"illuminate/view": "^10.45",
"illuminate/auth": "^10.45|^11.0",
"illuminate/console": "^10.45|^11.0",
"illuminate/contracts": "^10.45|^11.0",
"illuminate/cookie": "^10.45|^11.0",
"illuminate/database": "^10.45|^11.0",
"illuminate/http": "^10.45|^11.0",
"illuminate/routing": "^10.45|^11.0",
"illuminate/session": "^10.45|^11.0",
"illuminate/support": "^10.45|^11.0",
"illuminate/view": "^10.45|^11.0",
"spatie/laravel-package-tools": "^1.9"
},
"autoload": {
+1 -1
View File
@@ -27,7 +27,7 @@ php artisan filament:install --panels
This will create and register a new [Laravel service provider](https://laravel.com/docs/providers) called `app/Providers/Filament/AdminPanelProvider.php`.
> If you get an error when accessing your panel, check that the service provider was registered in your `config/app.php`. If not, you should manually add it to the `providers` array.
> If you get an error when accessing your panel, check that the service provider was registered in `bootstrap/providers.php` (Laravel 11 and above) or `config/app.php` (Laravel 10 and below). If not, you should manually add it.
## Create a user
You can create a new user account with the following command:
@@ -60,8 +60,6 @@ If you'd like to save time, Filament can automatically generate the [form](#reso
php artisan make:filament-resource Customer --generate
```
> If your table contains ENUM columns, the `doctrine/dbal` package we use is unable to scan your table and will crash. Hence, Filament is unable to generate the schema for your resource if it contains an ENUM column. Read more about this issue [here](https://github.com/doctrine/dbal/issues/3819#issuecomment-573419808).
### Handling soft deletes
By default, you will not be able to interact with deleted records in the app. If you'd like to add functionality to restore, force delete and filter trashed records in your resource, use the `--soft-deletes` flag when generating the resource:
+1 -1
View File
@@ -30,7 +30,7 @@ php artisan make:filament-panel app
This command will create a new panel called "app". A configuration file will be created at `app/Providers/Filament/AppPanelProvider.php`. You can access this panel at `/app`, but you can [customize the path](#changing-the-path) if you don't want that.
Since this configuration file is also a [Laravel service provider](https://laravel.com/docs/providers), it needs to be registered in `config/app.php`. Filament will attempt to do this for you, but if you get an error while trying to access your panel then this process has probably failed. You can manually register the service provider by adding it to the `providers` array.
Since this configuration file is also a [Laravel service provider](https://laravel.com/docs/providers), it needs to be registered in `bootstrap/providers.php` (Laravel 11 and above) or `config/app.php` (Laravel 10 and below). Filament will attempt to do this for you, but if you get an error while trying to access your panel then this process has probably failed.
## Changing the path
@@ -9,8 +9,8 @@
},
"require": {
"php": "^8.1",
"illuminate/contracts": "^10.45",
"illuminate/support": "^10.45",
"illuminate/contracts": "^10.45|^11.0",
"illuminate/support": "^10.45|^11.0",
"spatie/laravel-google-fonts": "^1.0"
},
"autoload": {
@@ -9,7 +9,7 @@
},
"require": {
"php": "^8.1",
"illuminate/support": "^10.45",
"illuminate/support": "^10.45|^11.0",
"spatie/laravel-medialibrary": "^10.0|^11.0"
},
"autoload": {
@@ -10,9 +10,9 @@
"require": {
"php": "^8.1",
"filament/filament": "self.version",
"illuminate/console": "^10.45",
"illuminate/filesystem": "^10.45",
"illuminate/support": "^10.45",
"illuminate/console": "^10.45|^11.0",
"illuminate/filesystem": "^10.45|^11.0",
"illuminate/support": "^10.45|^11.0",
"spatie/laravel-settings": "^2.2|^3.0"
},
"autoload": {
@@ -9,7 +9,7 @@
},
"require": {
"php": "^8.1",
"illuminate/database": "^10.45",
"illuminate/database": "^10.45|^11.0",
"spatie/laravel-tags": "^4.0"
},
"autoload": {
@@ -10,7 +10,7 @@
"require": {
"php": "^8.1",
"filament/support": "self.version",
"illuminate/support": "^10.45",
"illuminate/support": "^10.45|^11.0",
"spatie/laravel-translatable": "^6.0"
},
"autoload": {
+4 -5
View File
@@ -10,13 +10,12 @@
"require": {
"php": "^8.1",
"blade-ui-kit/blade-heroicons": "^2.2.1",
"doctrine/dbal": "^3.2",
"ext-intl": "*",
"illuminate/contracts": "^10.45",
"illuminate/support": "^10.45",
"illuminate/view": "^10.45",
"illuminate/contracts": "^10.45|^11.0",
"illuminate/support": "^10.45|^11.0",
"illuminate/view": "^10.45|^11.0",
"livewire/livewire": "^3.2.3",
"ryangjchandler/blade-capture-directive": "^0.2|^0.3",
"ryangjchandler/blade-capture-directive": "^0.2|^0.3|^1.0",
"spatie/color": "^1.5",
"spatie/invade": "^1.0|^2.0",
"spatie/laravel-package-tools": "^1.9",
@@ -3,6 +3,8 @@
namespace Filament\Support\Commands\Concerns;
use Filament\Facades\Filament;
use Illuminate\Support\Facades\App;
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Str;
use function Laravel\Prompts\text;
@@ -48,18 +50,36 @@ trait CanGeneratePanels
]);
}
$appConfig = file_get_contents(config_path('app.php'));
$isLaravel11OrHigherWithBootstrapProvidersFile = version_compare(App::version(), '11.0', '>=') &&
/** @phpstan-ignore-next-line */
file_exists($bootstrapProvidersPath = App::getBootstrapProvidersPath());
if (! Str::contains($appConfig, "App\\Providers\\Filament\\{$class}::class")) {
file_put_contents(config_path('app.php'), str_replace(
'App\\Providers\\RouteServiceProvider::class,',
"App\\Providers\\Filament\\{$class}::class," . PHP_EOL . ' App\\Providers\\RouteServiceProvider::class,',
$appConfig,
));
if ($isLaravel11OrHigherWithBootstrapProvidersFile) {
/** @phpstan-ignore-next-line */
ServiceProvider::addProviderToBootstrapFile(
"App\\Providers\\Filament\\{$class}",
/** @phpstan-ignore-next-line */
$bootstrapProvidersPath,
);
} else {
$appConfig = file_get_contents(config_path('app.php'));
if (! Str::contains($appConfig, "App\\Providers\\Filament\\{$class}::class")) {
file_put_contents(config_path('app.php'), str_replace(
'App\\Providers\\RouteServiceProvider::class,',
"App\\Providers\\Filament\\{$class}::class," . PHP_EOL . ' App\\Providers\\RouteServiceProvider::class,',
$appConfig,
));
}
}
$this->components->info("Filament panel [{$path}] created successfully.");
$this->components->warn("We've attempted to register the {$class} in your [config/app.php] file as a service provider. If you get an error while trying to access your panel then this process has probably failed. You can manually register the service provider by adding it to the [providers] array.");
if ($isLaravel11OrHigherWithBootstrapProvidersFile) {
$this->components->warn("We've attempted to register the {$class} in your [bootstrap/providers.php] file. If you get an error while trying to access your panel then this process has probably failed. You can manually register the service provider by adding it to the array.");
} else {
$this->components->warn("We've attempted to register the {$class} in your [config/app.php] file as a service provider. If you get an error while trying to access your panel then this process has probably failed. You can manually register the service provider by adding it to the [providers] array.");
}
return true;
}
@@ -2,15 +2,12 @@
namespace Filament\Support\Commands\Concerns;
use Doctrine\DBAL\Schema\AbstractAsset;
use Doctrine\DBAL\Schema\Table;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Str;
use ReflectionClass;
use ReflectionException;
use Throwable;
trait CanReadModelSchemas
{
@@ -23,29 +20,24 @@ trait CanReadModelSchemas
return $model;
}
protected function getModelTable(string $model): ?Table
protected function getModelSchema(string $model): Schema
{
$modelClass = $model;
$model = app($model);
try {
return $model
->getConnection()
->getDoctrineSchemaManager()
->listTableDetails($model->getTable());
} catch (Throwable $exception) {
$this->components->warn("Unable to read table schema for model [{$modelClass}]: {$exception->getMessage()}");
return null;
}
return app($model)
->getConnection()
->getSchemaBuilder();
}
protected function guessBelongsToRelationshipName(AbstractAsset $column, string $model): ?string
protected function getModelTable(string $model): string
{
return app($model)->getTable();
}
protected function guessBelongsToRelationshipName(string $column, string $model): ?string
{
/** @var Model $modelInstance */
$modelInstance = app($model);
$modelInstanceReflection = new ReflectionClass($modelInstance);
$guessedRelationshipName = str($column->getName())->beforeLast('_id');
$guessedRelationshipName = str($column)->beforeLast('_id');
$hasRelationship = $modelInstanceReflection->hasMethod($guessedRelationshipName);
if (! $hasRelationship) {
@@ -74,9 +66,9 @@ trait CanReadModelSchemas
return $guessedRelationshipName;
}
protected function guessBelongsToRelationshipTableName(AbstractAsset $column): ?string
protected function guessBelongsToRelationshipTableName(string $column): ?string
{
$tableName = str($column->getName())->beforeLast('_id');
$tableName = str($column)->beforeLast('_id');
if (Schema::hasTable(Str::plural($tableName))) {
return Str::plural($tableName);
@@ -89,15 +81,12 @@ trait CanReadModelSchemas
return $tableName;
}
protected function guessBelongsToRelationshipTitleColumnName(AbstractAsset $column, string $model): string
protected function guessBelongsToRelationshipTitleColumnName(string $column, string $model): string
{
$schema = $this->getModelTable($model);
$schema = $this->getModelSchema($model);
$table = $this->getModelTable($model);
if ($schema === null) {
return 'id';
}
$columns = collect(array_keys($schema->getColumns()));
$columns = collect($schema->getColumnListing($table));
if ($columns->contains('name')) {
return 'name';
@@ -107,6 +96,114 @@ trait CanReadModelSchemas
return 'title';
}
return $schema->getPrimaryKey()?->getColumns()[0] ?? 'id';
return collect($schema->getIndexes($table))
->firstWhere('primary')['columns'][0] ?? 'id';
}
/**
* @param array<string, mixed> $column
* @return array<string, mixed>
*/
protected function parseColumnType(array $column): array
{
$type = match ($column['type']) {
'tinyint(1)', 'bit' => 'boolean',
'varchar(max)', 'nvarchar(max)' => 'text',
default => null,
};
$type ??= match ($column['type_name']) {
'boolean', 'bool' => 'boolean',
'char', 'bpchar', 'nchar' => 'char',
'varchar', 'nvarchar' => 'string',
'integer', 'int', 'int4', 'tinyint', 'smallint', 'int2', 'mediumint', 'bigint', 'int8' => 'integer',
'decimal', 'numeric' => 'decimal',
'float', 'real', 'float4' => 'float',
'double', 'float8' => 'double',
'money', 'smallmoney' => 'money',
'date' => 'date',
'time', 'timetz' => 'time',
'datetime', 'datetime2', 'smalldatetime', 'datetimeoffset' => 'datetime',
'timestamp', 'timestamptz' => 'timestamp',
'text', 'tinytext', 'longtext', 'mediumtext', 'ntext' => 'text',
'json', 'jsonb' => 'json',
default => $column['type_name'],
};
$values = str_contains($column['type'], '(')
? str_getcsv(Str::between($column['type'], '(', ')'), ',', "'")
: null;
$values = is_null($values) ? [] : match ($type) {
'string', 'char', 'binary', 'bit' => ['length' => (int) $values[0]],
default => [],
};
return array_merge(['name' => $type], array_filter($values));
}
/**
* @param array<string, mixed> $column
*/
protected function parseDefaultExpression(array $column, string $model): mixed
{
$default = $column['default'];
if (blank($default)) {
return null;
}
$driver = app($model)->getConnection()->getDriverName();
if ($driver === 'mysql') {
if ($default === 'NULL'
|| preg_match("/^\(.*\)$/", $default) === 1
|| str_ends_with($default, '()')
|| str_starts_with(strtolower($default), 'current_timestamp')) {
return null;
}
if (preg_match("/^'(.*)'$/", $default, $matches) === 1) {
return str_replace("''", "'", $matches[1]);
}
}
if ($driver === 'pgsql') {
if (str_starts_with($default, 'NULL::')) {
$default = null;
}
if (preg_match("/^['(](.*)[')]::/", $default, $matches) === 1) {
return str_replace("''", "'", $matches[1]);
}
}
if ($driver === 'sqlsrv') {
while (preg_match('/^\((.*)\)$/', $default, $matches)) {
$default = $matches[1];
}
if ($default === 'NULL'
|| str_ends_with($default, '()')) {
return null;
}
if (preg_match('/^\'(.*)\'$/', $default, $matches) === 1) {
return str_replace("''", "'", $matches[1]);
}
}
if ($driver === 'sqlite') {
if ($default === 'NULL'
|| str_starts_with(strtolower($default), 'current_timestamp')) {
return null;
}
if (preg_match('/^\'(.*)\'$/s', $default, $matches) === 1) {
return str_replace("''", "'", $matches[1]);
}
}
return $default;
}
}
+1 -1
View File
@@ -2,8 +2,8 @@
namespace Livewire\Features\SupportTesting {
use Illuminate\Support\Collection;
use Closure;
use Illuminate\Support\Collection;
class Testable {
public function mountTableAction(string | array $name, $record = null): static {}
+6 -6
View File
@@ -12,12 +12,12 @@
"filament/actions": "self.version",
"filament/forms": "self.version",
"filament/support": "self.version",
"illuminate/console": "^10.45",
"illuminate/contracts": "^10.45",
"illuminate/database": "^10.45",
"illuminate/filesystem": "^10.45",
"illuminate/support": "^10.45",
"illuminate/view": "^10.45",
"illuminate/console": "^10.45|^11.0",
"illuminate/contracts": "^10.45|^11.0",
"illuminate/database": "^10.45|^11.0",
"illuminate/filesystem": "^10.45|^11.0",
"illuminate/support": "^10.45|^11.0",
"illuminate/view": "^10.45|^11.0",
"kirschbaum-development/eloquent-power-joins": "^3.0",
"spatie/laravel-package-tools": "^1.9"
},
@@ -135,5 +135,3 @@ Filament is also able to guess which table columns you want in the table, based
```bash
php artisan make:livewire-table Products/ListProducts --generate
```
> If your table contains ENUM columns, the `doctrine/dbal` package we use is unable to scan your table and will crash. Hence, Filament is unable to generate the schema for your table if it contains an ENUM column. Read more about this issue [here](https://github.com/doctrine/dbal/issues/3819#issuecomment-573419808).
@@ -2,7 +2,6 @@
namespace Filament\Tables\Commands\Concerns;
use Doctrine\DBAL\Types;
use Filament\Tables;
use Illuminate\Support\Str;
@@ -16,27 +15,26 @@ trait CanGenerateTables
return '//';
}
$schema = $this->getModelSchema($model);
$table = $this->getModelTable($model);
if (blank($table)) {
return '//';
}
$columns = [];
foreach ($table->getColumns() as $column) {
if ($column->getAutoincrement()) {
foreach ($schema->getColumns($table) as $column) {
if ($column['auto_increment']) {
continue;
}
if (in_array($column->getType()::class, [
Types\JsonType::class,
Types\TextType::class,
$type = $this->parseColumnType($column);
if (in_array($type['name'], [
'json',
'text',
])) {
continue;
}
$columnName = $column->getName();
$columnName = $column['name'];
if (str($columnName)->endsWith([
'_token',
@@ -51,10 +49,10 @@ trait CanGenerateTables
}
if (str($columnName)->endsWith('_id')) {
$guessedRelationshipName = $this->guessBelongsToRelationshipName($column, $model);
$guessedRelationshipName = $this->guessBelongsToRelationshipName($columnName, $model);
if (filled($guessedRelationshipName)) {
$guessedRelationshipTitleColumnName = $this->guessBelongsToRelationshipTitleColumnName($column, app($model)->{$guessedRelationshipName}()->getModel()::class);
$guessedRelationshipTitleColumnName = $this->guessBelongsToRelationshipTitleColumnName($columnName, app($model)->{$guessedRelationshipName}()->getModel()::class);
$columnName = "{$guessedRelationshipName}.{$guessedRelationshipTitleColumnName}";
}
@@ -70,7 +68,7 @@ trait CanGenerateTables
$columnData['label'] = [Str::upper($columnName)];
}
if ($column->getType() instanceof Types\BooleanType) {
if ($type['name'] === 'boolean') {
$columnData['type'] = Tables\Columns\IconColumn::class;
$columnData['boolean'] = [];
} else {
@@ -79,42 +77,40 @@ trait CanGenerateTables
default => Tables\Columns\TextColumn::class,
};
if (in_array($column->getType()::class, [
Types\StringType::class,
if (in_array($type['name'], [
'string',
'char',
]) && ($columnData['type'] === Tables\Columns\TextColumn::class)) {
$columnData['searchable'] = [];
}
if (in_array($column->getType()::class, [
Types\DateImmutableType::class,
Types\DateType::class,
if (in_array($type['name'], [
'date',
])) {
$columnData['date'] = [];
$columnData['sortable'] = [];
}
if (in_array($column->getType()::class, [
Types\DateTimeImmutableType::class,
Types\DateTimeType::class,
Types\DateTimeTzImmutableType::class,
Types\DateTimeTzType::class,
if (in_array($type['name'], [
'datetime',
'timestamp',
])) {
$columnData['dateTime'] = [];
$columnData['sortable'] = [];
}
if (in_array($column->getType()::class, [
Types\BigIntType::class,
Types\DecimalType::class,
Types\FloatType::class,
Types\IntegerType::class,
Types\SmallIntType::class,
if (in_array($type['name'], [
'integer',
'decimal',
'float',
'double',
'money',
])) {
$columnData[in_array($columnName, [
'cost',
'money',
'price',
]) ? 'money' : 'numeric'] = [];
]) || $type['name'] === 'money' ? 'money' : 'numeric'] = [];
$columnData['sortable'] = [];
}
}
+2 -2
View File
@@ -9,8 +9,8 @@
},
"require": {
"php": "^8.1",
"nunomaduro/termwind": "^1.0",
"rector/rector": "^0.17"
"nunomaduro/termwind": "^1.0|^2.0",
"rector/rector": "^0.19"
},
"autoload": {
"psr-4": {
+3
View File
@@ -4,6 +4,9 @@ parameters:
paths:
- packages
excludePaths:
- packages/upgrade/*
ignoreErrors:
- '#Unable to resolve the template type TKey in call to function collect#'
- '#Unable to resolve the template type TValue in call to function collect#'
+5
View File
@@ -0,0 +1,5 @@
migrations:
- tests/database/migrations
workbench:
install: true
@@ -1,22 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class() extends Migration
{
public function up(): void
{
Schema::create('password_reset_tokens', function (Blueprint $table): void {
$table->string('email')->index();
$table->string('token');
$table->timestamp('created_at')->nullable();
});
}
public function down(): void
{
Schema::dropIfExists('password_reset_tokens');
}
};
@@ -1,26 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class() extends Migration
{
public function up(): void
{
Schema::create('users', function (Blueprint $table): void {
$table->id();
$table->string('name');
$table->string('email')->unique();
$table->timestamp('email_verified_at')->nullable();
$table->string('password');
$table->rememberToken();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('users');
}
};
+3 -7
View File
@@ -17,12 +17,14 @@ use Filament\Tests\Models\User;
use Filament\Widgets\WidgetsServiceProvider;
use Illuminate\Foundation\Testing\LazilyRefreshDatabase;
use Livewire\LivewireServiceProvider;
use Orchestra\Testbench\Concerns\WithWorkbench;
use Orchestra\Testbench\TestCase as BaseTestCase;
use RyanChandler\BladeCaptureDirective\BladeCaptureDirectiveServiceProvider;
abstract class TestCase extends BaseTestCase
{
use LazilyRefreshDatabase;
use WithWorkbench;
protected function getPackageProviders($app): array
{
@@ -49,14 +51,8 @@ abstract class TestCase extends BaseTestCase
];
}
protected function defineDatabaseMigrations(): void
protected function defineEnvironment($app): void
{
$this->loadMigrationsFrom(__DIR__ . '/../database/migrations');
}
protected function getEnvironmentSetUp($app): void
{
$app['config']->set('auth.passwords.users.table', 'password_reset_tokens');
$app['config']->set('auth.providers.users.model', User::class);
$app['config']->set('view.paths', [
...$app['config']->get('view.paths'),
+23 -23
View File
@@ -1,25 +1,25 @@
<?php
namespace Filament\Tests\Upgrade;
use Iterator;
use Rector\Testing\PHPUnit\AbstractRectorTestCase;
class RectorTest extends AbstractRectorTestCase
{
/** @dataProvider provideData */
public function test(string $filePath): void
{
$this->doTestFile($filePath);
}
public static function provideData(): Iterator
{
return self::yieldFilesFromDirectory(__DIR__ . '/Fixture');
}
public function provideConfigFilePath(): string
{
return __DIR__ . '/../../../packages/upgrade/src/rector.php';
}
}
// namespace Filament\Tests\Upgrade;
//
// use Iterator;
// use Rector\Testing\PHPUnit\AbstractRectorTestCase;
//
// class RectorTest extends AbstractRectorTestCase
// {
// /** @dataProvider provideData */
// public function test(string $filePath): void
// {
// $this->doTestFile($filePath);
// }
//
// public static function provideData(): Iterator
// {
// return self::yieldFilesFromDirectory(__DIR__ . '/Fixture');
// }
//
// public function provideConfigFilePath(): string
// {
// return __DIR__ . '/../../../packages/upgrade/src/rector.php';
// }
// }