🆙 Add cms i using 🆙

This commit is contained in:
2025-11-25 22:42:56 +01:00
parent 94704e0925
commit d44196149e
35591 changed files with 3601123 additions and 0 deletions
@@ -0,0 +1,234 @@
<?php
namespace Filament\Actions\Commands\FileGenerators;
use Filament\Actions\Exports\ExportColumn;
use Filament\Actions\Exports\Exporter;
use Filament\Actions\Exports\Models\Export;
use Filament\Support\Commands\Concerns\CanReadModelSchemas;
use Filament\Support\Commands\FileGenerators\ClassGenerator;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Number;
use Illuminate\Support\Str;
use Nette\PhpGenerator\ClassType;
use Nette\PhpGenerator\Literal;
use Nette\PhpGenerator\Method;
use Nette\PhpGenerator\Property;
use function Filament\Support\get_model_label;
class ExporterClassGenerator extends ClassGenerator
{
use CanReadModelSchemas;
/**
* @param class-string<Model> $modelFqn
*/
final public function __construct(
protected string $fqn,
protected string $modelFqn,
protected bool $isGenerated,
) {}
public function getNamespace(): string
{
return $this->extractNamespace($this->getFqn());
}
/**
* @return array<string>
*/
public function getImports(): array
{
return [
$this->getExtends(),
Export::class,
ExportColumn::class,
Number::class,
$this->getModelFqn(),
];
}
public function getBasename(): string
{
return class_basename($this->getFqn());
}
public function getExtends(): string
{
return Exporter::class;
}
protected function addPropertiesToClass(ClassType $class): void
{
$this->addModelPropertyToClass($class);
}
protected function addMethodsToClass(ClassType $class): void
{
$this->addGetColumnsMethodToClass($class);
$this->addGetCompletedNotificationBodyMethodToClass($class);
}
protected function addModelPropertyToClass(ClassType $class): void
{
$property = $class->addProperty('model', new Literal("{$this->simplifyFqn($this->getModelFqn())}::class"))
->setProtected()
->setStatic()
->setType('?string');
$this->configureModelProperty($property);
}
protected function configureModelProperty(Property $property): void {}
protected function addGetColumnsMethodToClass(ClassType $class): void
{
$method = $class->addMethod('getColumns')
->setPublic()
->setStatic()
->setReturnType('array')
->setBody(<<<PHP
return [
{$this->outputExporterColumns()}
];
PHP);
$this->configureGetColumnsMethod($method);
}
public function outputExporterColumns(): string
{
$columns = $this->getExporterColumns();
if (empty($columns)) {
return '//';
}
return implode(PHP_EOL . ' ', $columns);
}
/**
* @return array<string>
*/
public function getExporterColumns(): array
{
if (! $this->isGenerated()) {
return [];
}
$model = $this->getModelFqn();
if (blank($model)) {
return [];
}
if (! class_exists($model)) {
return [];
}
$schema = $this->getModelSchema($model);
$table = $this->getModelTable($model);
$columns = [];
foreach ($schema->getColumns($table) as $column) {
$columnName = $column['name'];
if (str($columnName)->endsWith([
'_token',
])) {
continue;
}
if (str($columnName)->contains([
'password',
])) {
continue;
}
if (str($columnName)->endsWith('_id')) {
$guessedRelationshipName = $this->guessBelongsToRelationshipName($columnName, $model);
if (filled($guessedRelationshipName)) {
$guessedRelationshipTitleColumnName = $this->guessBelongsToRelationshipTitleColumnName($columnName, app($model)->{$guessedRelationshipName}()->getModel()::class);
$columnName = "{$guessedRelationshipName}.{$guessedRelationshipTitleColumnName}";
}
}
$columnData = [];
if (in_array($columnName, [
'id',
'sku',
'uuid',
])) {
$columnData['label'] = [Str::upper($columnName)];
}
$columns[$columnName] = $columnData;
}
return array_map(
function (array $columnData, string $columnName): string {
$column = (string) new Literal("{$this->simplifyFqn(ExportColumn::class)}::make(?)", [$columnName]);
foreach ($columnData as $methodName => $parameters) {
$column .= new Literal(PHP_EOL . " ->{$methodName}(...?:)", [$parameters]);
}
return "{$column},";
},
$columns,
array_keys($columns),
);
}
protected function configureGetColumnsMethod(Method $method): void {}
protected function addGetCompletedNotificationBodyMethodToClass(ClassType $class): void
{
$method = $class->addMethod('getCompletedNotificationBody')
->setPublic()
->setStatic()
->setReturnType('string')
->setBody(<<<PHP
\$body = 'Your {$this->getModelLabel()} export has completed and ' . {$this->simplifyFqn(Number::class)}::format(\$export->successful_rows) . ' ' . str('row')->plural(\$export->successful_rows) . ' exported.';
if (\$failedRowsCount = \$export->getFailedRowsCount()) {
\$body .= ' ' . {$this->simplifyFqn(Number::class)}::format(\$failedRowsCount) . ' ' . str('row')->plural(\$failedRowsCount) . ' failed to export.';
}
return \$body;
PHP);
$method->addParameter('export')
->setType(Export::class);
$this->configureGetCompletedNotificationBodyMethod($method);
}
protected function configureGetCompletedNotificationBodyMethod(Method $method): void {}
public function getFqn(): string
{
return $this->fqn;
}
public function getModelLabel(): string
{
return get_model_label($this->getModelFqn());
}
/**
* @return class-string<Model>
*/
public function getModelFqn(): string
{
return $this->modelFqn;
}
public function isGenerated(): bool
{
return $this->isGenerated;
}
}
@@ -0,0 +1,323 @@
<?php
namespace Filament\Actions\Commands\FileGenerators;
use Filament\Actions\Imports\ImportColumn;
use Filament\Actions\Imports\Importer;
use Filament\Actions\Imports\Models\Import;
use Filament\Support\Commands\Concerns\CanReadModelSchemas;
use Filament\Support\Commands\FileGenerators\ClassGenerator;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Number;
use Illuminate\Support\Str;
use Nette\PhpGenerator\ClassType;
use Nette\PhpGenerator\Literal;
use Nette\PhpGenerator\Method;
use Nette\PhpGenerator\Property;
use function Filament\Support\get_model_label;
class ImporterClassGenerator extends ClassGenerator
{
use CanReadModelSchemas;
/**
* @param class-string<Model> $modelFqn
*/
final public function __construct(
protected string $fqn,
protected string $modelFqn,
protected string $resolutionMode,
protected ?string $resolutionColumn,
protected bool $isGenerated,
) {}
public function getNamespace(): string
{
return $this->extractNamespace($this->getFqn());
}
/**
* @return array<string>
*/
public function getImports(): array
{
return [
$this->getExtends(),
Import::class,
ImportColumn::class,
Number::class,
$this->getModelFqn(),
];
}
public function getBasename(): string
{
return class_basename($this->getFqn());
}
public function getExtends(): string
{
return Importer::class;
}
protected function addPropertiesToClass(ClassType $class): void
{
$this->addModelPropertyToClass($class);
}
protected function addMethodsToClass(ClassType $class): void
{
$this->addGetColumnsMethodToClass($class);
$this->addResolveRecordMethodToClass($class);
$this->addGetCompletedNotificationBodyMethodToClass($class);
}
protected function addModelPropertyToClass(ClassType $class): void
{
$property = $class->addProperty('model', new Literal("{$this->simplifyFqn($this->getModelFqn())}::class"))
->setProtected()
->setStatic()
->setType('?string');
$this->configureModelProperty($property);
}
protected function configureModelProperty(Property $property): void {}
protected function addGetColumnsMethodToClass(ClassType $class): void
{
$method = $class->addMethod('getColumns')
->setPublic()
->setStatic()
->setReturnType('array')
->setBody(<<<PHP
return [
{$this->outputImporterColumns()}
];
PHP);
$this->configureGetColumnsMethod($method);
}
public function outputImporterColumns(): string
{
$columns = $this->getImporterColumns();
if (empty($columns)) {
return '//';
}
return implode(PHP_EOL . ' ', $columns);
}
/**
* @return array<string>
*/
public function getImporterColumns(): array
{
if (! $this->isGenerated()) {
return [];
}
$model = $this->getModelFqn();
if (blank($model)) {
return [];
}
if (! class_exists($model)) {
return [];
}
$schema = $this->getModelSchema($model);
$table = $this->getModelTable($model);
$columns = [];
foreach ($schema->getColumns($table) as $column) {
if ($column['auto_increment']) {
continue;
}
$columnName = $column['name'];
if (str($columnName)->is([
app($model)->getKeyName(),
'created_at',
'deleted_at',
'updated_at',
'*_token',
])) {
continue;
}
$columnData = [];
if (in_array($columnName, [
'id',
'sku',
'uuid',
])) {
$columnData['label'] = [Str::upper($columnName)];
}
if (! $column['nullable']) {
$columnData['rules'][0][] = 'required';
$columnData['requiredMapping'] = [];
}
if (str($columnName)->contains(['email'])) {
$columnData['rules'][0][] = 'email';
}
$type = $this->parseColumnType($column);
if (
str($columnName)->endsWith('_id') &&
filled($guessedRelationshipName = $this->guessBelongsToRelationshipName($columnName, $model))
) {
$columnName = $guessedRelationshipName;
$columnData['relationship'] = [];
} elseif (in_array($type['name'], [
'boolean',
])) {
$columnData['rules'][0][] = 'boolean';
$columnData['boolean'] = [];
} elseif (in_array($type['name'], [
'date',
])) {
$columnData['rules'][0][] = 'date';
} elseif (in_array($type['name'], [
'datetime',
'timestamp',
])) {
$columnData['rules'][0][] = 'datetime';
} elseif (in_array($type['name'], [
'integer',
'decimal',
'float',
'double',
'money',
])) {
$columnData['rules'][0][] = 'integer';
$columnData['numeric'] = [];
} elseif (isset($type['length'])) {
$columnData['rules'][0][] = "max:{$type['length']}";
}
// Move rules to the end of the column definition.
if (array_key_exists('rules', $columnData)) {
$rules = $columnData['rules'];
unset($columnData['rules']);
$columnData['rules'] = $rules;
}
$columns[$columnName] = $columnData;
}
return array_map(
function (array $columnData, string $columnName): string {
$column = (string) new Literal("{$this->simplifyFqn(ImportColumn::class)}::make(?)", [$columnName]);
foreach ($columnData as $methodName => $parameters) {
$column .= new Literal(PHP_EOL . " ->{$methodName}(...?:)", [$parameters]);
}
return "{$column},";
},
$columns,
array_keys($columns),
);
}
protected function configureGetColumnsMethod(Method $method): void {}
protected function addResolveRecordMethodToClass(ClassType $class): void
{
$resolutionMode = $this->getResolutionMode();
$resolutionColumn = $this->getResolutionColumn();
$method = $class->addMethod('resolveRecord')
->setPublic()
->setReturnType(match ($resolutionMode) {
'update' => "?{$this->getModelFqn()}",
default => $this->getModelFqn(),
})
->setBody(match ($resolutionMode) {
'upsert' => new Literal(<<<PHP
return {$this->simplifyFqn($this->getModelFqn())}::firstOrNew([
? => \$this->data[?],
]);
PHP, [$resolutionColumn, $resolutionColumn]),
'update' => new Literal(<<<PHP
return {$this->simplifyFqn($this->getModelFqn())}::query()
->where(?, \$this->data[?])
->first();
PHP, [$resolutionColumn, $resolutionColumn]),
default => <<<PHP
return new {$this->simplifyFqn($this->getModelFqn())}();
PHP,
});
$this->configureResolveRecordMethod($method);
}
protected function configureResolveRecordMethod(Method $method): void {}
protected function addGetCompletedNotificationBodyMethodToClass(ClassType $class): void
{
$method = $class->addMethod('getCompletedNotificationBody')
->setPublic()
->setStatic()
->setReturnType('string')
->setBody(<<<PHP
\$body = 'Your {$this->getModelLabel()} import has completed and ' . {$this->simplifyFqn(Number::class)}::format(\$import->successful_rows) . ' ' . str('row')->plural(\$import->successful_rows) . ' imported.';
if (\$failedRowsCount = \$import->getFailedRowsCount()) {
\$body .= ' ' . {$this->simplifyFqn(Number::class)}::format(\$failedRowsCount) . ' ' . str('row')->plural(\$failedRowsCount) . ' failed to import.';
}
return \$body;
PHP);
$method->addParameter('import')
->setType(Import::class);
$this->configureGetCompletedNotificationBodyMethod($method);
}
protected function configureGetCompletedNotificationBodyMethod(Method $method): void {}
public function getFqn(): string
{
return $this->fqn;
}
public function getModelLabel(): string
{
return get_model_label($this->getModelFqn());
}
/**
* @return class-string<Model>
*/
public function getModelFqn(): string
{
return $this->modelFqn;
}
public function getResolutionMode(): string
{
return $this->resolutionMode;
}
public function getResolutionColumn(): ?string
{
return $this->resolutionColumn;
}
public function isGenerated(): bool
{
return $this->isGenerated;
}
}
@@ -0,0 +1,199 @@
<?php
namespace Filament\Actions\Commands;
use Filament\Actions\Commands\FileGenerators\ExporterClassGenerator;
use Filament\Support\Commands\Concerns\CanAskForComponentLocation;
use Filament\Support\Commands\Concerns\CanManipulateFiles;
use Filament\Support\Commands\Exceptions\FailureCommandOutput;
use Illuminate\Console\Command;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Stringable;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
use function Filament\Support\discover_app_classes;
use function Laravel\Prompts\confirm;
use function Laravel\Prompts\suggest;
#[AsCommand(name: 'make:filament-exporter', aliases: [
'filament:exporter',
])]
class MakeExporterCommand extends Command
{
use CanAskForComponentLocation;
use CanManipulateFiles;
protected $description = 'Create a new Filament exporter class';
protected $name = 'make:filament-exporter';
/**
* @var class-string<Model>
*/
protected string $modelFqn;
protected string $modelFqnEnd;
protected bool $isGenerated;
/**
* @var class-string
*/
protected string $fqn;
protected string $fqnEnd;
protected string $path;
/**
* @var array<string>
*/
protected $aliases = [
'filament:exporter',
];
/**
* @return array<InputArgument>
*/
protected function getArguments(): array
{
return [
new InputArgument(
name: 'model',
mode: InputArgument::OPTIONAL,
description: 'The name of the model to generate the exporter for, optionally prefixed with directories',
),
];
}
/**
* @return array<InputOption>
*/
protected function getOptions(): array
{
return [
new InputOption(
name: 'generate',
shortcut: 'G',
mode: InputOption::VALUE_NONE,
description: 'Generate the exporter columns based on the attributes of a model',
),
new InputOption(
name: 'model-namespace',
shortcut: null,
mode: InputOption::VALUE_REQUIRED,
description: 'The namespace of the model class, [' . app()->getNamespace() . 'Models] by default',
),
new InputOption(
name: 'force',
shortcut: 'F',
mode: InputOption::VALUE_NONE,
description: 'Overwrite the contents of the files if they already exist',
),
];
}
public function handle(): int
{
try {
$this->configureModel();
$this->configureIsGenerated();
$this->configureLocation();
$this->createExporter();
} catch (FailureCommandOutput) {
return static::FAILURE;
}
$this->components->info("Exporter [{$this->fqn}] created successfully.");
return static::SUCCESS;
}
protected function configureModel(): void
{
if ($this->argument('model')) {
$this->modelFqnEnd = (string) str($this->argument('model'))
->trim('/')
->trim('\\')
->trim(' ')
->when(
fn (Stringable $model): bool => str($model)->endsWith('Exporter'),
fn (Stringable $model): Stringable => str($model)->beforeLast('Exporter'),
)
->studly()
->replace('/', '\\');
if (blank($this->modelFqnEnd)) {
$this->modelFqnEnd = 'Resource';
}
$modelNamespace = $this->option('model-namespace') ?? app()->getNamespace() . 'Models';
$this->modelFqn = "{$modelNamespace}\\{$this->modelFqnEnd}";
} else {
$modelFqns = discover_app_classes(parentClass: Model::class);
$this->modelFqn = suggest(
label: 'What is the model?',
options: function (string $search) use ($modelFqns): array {
$search = str($search)->trim()->replace(['\\', '/'], '');
if (blank($search)) {
return $modelFqns;
}
return array_filter(
$modelFqns,
fn (string $class): bool => str($class)->replace(['\\', '/'], '')->contains($search, ignoreCase: true),
);
},
placeholder: app()->getNamespace() . 'Models\\BlogPost',
required: true,
);
$this->modelFqnEnd = class_basename($this->modelFqn);
}
}
protected function configureIsGenerated(): void
{
$this->isGenerated = $this->option('generate') || confirm(
label: 'Should the exporter columns be generated from the current database columns?',
default: false,
);
}
protected function configureLocation(): void
{
[
$namespace,
$path,
] = $this->askForComponentLocation(
path: 'Exports',
question: 'Where would you like to create the exporter?',
);
$this->fqnEnd = "{$this->modelFqnEnd}Exporter";
$this->fqn = "{$namespace}\\{$this->fqnEnd}";
$this->path = (string) str("{$path}\\{$this->fqnEnd}.php")
->replace('\\', '/')
->replace('//', '/');
}
protected function createExporter(): void
{
if (! $this->option('force') && $this->checkForCollision($this->path)) {
throw new FailureCommandOutput;
}
$this->writeFile($this->path, app(ExporterClassGenerator::class, [
'fqn' => $this->fqn,
'modelFqn' => $this->modelFqn,
'isGenerated' => $this->isGenerated,
]));
}
}
@@ -0,0 +1,229 @@
<?php
namespace Filament\Actions\Commands;
use Filament\Actions\Commands\FileGenerators\ImporterClassGenerator;
use Filament\Support\Commands\Concerns\CanAskForComponentLocation;
use Filament\Support\Commands\Concerns\CanManipulateFiles;
use Filament\Support\Commands\Exceptions\FailureCommandOutput;
use Illuminate\Console\Command;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Stringable;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
use function Filament\Support\discover_app_classes;
use function Laravel\Prompts\confirm;
use function Laravel\Prompts\select;
use function Laravel\Prompts\suggest;
use function Laravel\Prompts\text;
#[AsCommand(name: 'make:filament-importer', aliases: [
'filament:importer',
])]
class MakeImporterCommand extends Command
{
use CanAskForComponentLocation;
use CanManipulateFiles;
protected $description = 'Create a new Filament importer class';
protected $name = 'make:filament-importer';
/**
* @var class-string<Model>
*/
protected string $modelFqn;
protected string $modelFqnEnd;
protected string $resolutionMode;
protected ?string $resolutionColumn = null;
protected bool $isGenerated;
/**
* @var class-string
*/
protected string $fqn;
protected string $fqnEnd;
protected string $path;
/**
* @var array<string>
*/
protected $aliases = [
'filament:importer',
];
/**
* @return array<InputArgument>
*/
protected function getArguments(): array
{
return [
new InputArgument(
name: 'model',
mode: InputArgument::OPTIONAL,
description: 'The name of the model to generate the importer for, optionally prefixed with directories',
),
];
}
/**
* @return array<InputOption>
*/
protected function getOptions(): array
{
return [
new InputOption(
name: 'generate',
shortcut: 'G',
mode: InputOption::VALUE_NONE,
description: 'Generate the importer columns based on the attributes of a model',
),
new InputOption(
name: 'model-namespace',
shortcut: null,
mode: InputOption::VALUE_REQUIRED,
description: 'The namespace of the model class, [' . app()->getNamespace() . 'Models] by default',
),
new InputOption(
name: 'force',
shortcut: 'F',
mode: InputOption::VALUE_NONE,
description: 'Overwrite the contents of the files if they already exist',
),
];
}
public function handle(): int
{
try {
$this->configureModel();
$this->configureResolution();
$this->configureIsGenerated();
$this->configureLocation();
$this->createImporter();
} catch (FailureCommandOutput) {
return static::FAILURE;
}
$this->components->info("Importer [{$this->fqn}] created successfully.");
return static::SUCCESS;
}
protected function configureModel(): void
{
if ($this->argument('model')) {
$this->modelFqnEnd = (string) str($this->argument('model'))
->trim('/')
->trim('\\')
->trim(' ')
->when(
fn (Stringable $model): bool => str($model)->endsWith('Importer'),
fn (Stringable $model): Stringable => str($model)->beforeLast('Importer'),
)
->studly()
->replace('/', '\\');
if (blank($this->modelFqnEnd)) {
$this->modelFqnEnd = 'Resource';
}
$modelNamespace = $this->option('model-namespace') ?? app()->getNamespace() . 'Models';
$this->modelFqn = "{$modelNamespace}\\{$this->modelFqnEnd}";
} else {
$modelFqns = discover_app_classes(parentClass: Model::class);
$this->modelFqn = suggest(
label: 'What is the model?',
options: function (string $search) use ($modelFqns): array {
$search = str($search)->trim()->replace(['\\', '/'], '');
if (blank($search)) {
return $modelFqns;
}
return array_filter(
$modelFqns,
fn (string $class): bool => str($class)->replace(['\\', '/'], '')->contains($search, ignoreCase: true),
);
},
placeholder: app()->getNamespace() . 'Models\\BlogPost',
required: true,
);
$this->modelFqnEnd = class_basename($this->modelFqn);
}
}
protected function configureResolution(): void
{
$this->resolutionMode = select(
label: 'How would you like the importer to behave?',
options: [
'create' => 'Create records only',
'upsert' => 'Create a record if it does not exist, otherwise update it',
'update' => 'Update records only',
],
default: 'create',
);
if (in_array($this->resolutionMode, ['upsert', 'update'])) {
$this->resolutionColumn = text(
label: 'What column should be used to find existing records?',
placeholder: 'slug',
required: true,
);
}
}
protected function configureIsGenerated(): void
{
$this->isGenerated = $this->option('generate') || confirm(
label: 'Should the importer columns be generated from the current database columns?',
default: false,
);
}
protected function configureLocation(): void
{
[
$namespace,
$path,
] = $this->askForComponentLocation(
path: 'Imports',
question: 'Where would you like to create the importer?',
);
$this->fqnEnd = "{$this->modelFqnEnd}Importer";
$this->fqn = "{$namespace}\\{$this->fqnEnd}";
$this->path = (string) str("{$path}\\{$this->fqnEnd}.php")
->replace('\\', '/')
->replace('//', '/');
}
protected function createImporter(): void
{
if (! $this->option('force') && $this->checkForCollision($this->path)) {
throw new FailureCommandOutput;
}
$this->writeFile($this->path, app(ImporterClassGenerator::class, [
'fqn' => $this->fqn,
'modelFqn' => $this->modelFqn,
'resolutionMode' => $this->resolutionMode,
'resolutionColumn' => $this->resolutionColumn,
'isGenerated' => $this->isGenerated,
]));
}
}