mirror of
https://github.com/filamentphp/filament.git
synced 2026-10-30 18:42:45 +08:00
Generate table columns
This commit is contained in:
@@ -24,7 +24,7 @@ class ClusterClassGenerator extends ClassGenerator
|
||||
public function getImports(): array
|
||||
{
|
||||
return [
|
||||
Cluster::class,
|
||||
$this->getExtends(),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ use Filament\Resources\Pages\EditRecord;
|
||||
use Filament\Resources\Pages\ManageRelatedRecords;
|
||||
use Filament\Resources\Pages\Page as ResourcePage;
|
||||
use Filament\Resources\Pages\ViewRecord;
|
||||
use Filament\Support\Commands\Concerns\CanAskForViewLocation;
|
||||
use Filament\Support\Commands\Concerns\CanManipulateFiles;
|
||||
use Filament\Support\Commands\Exceptions\InvalidCommandOutput;
|
||||
use Filament\Support\Commands\FileGenerators\Concerns\CanCheckFileGenerationFlags;
|
||||
@@ -34,6 +35,7 @@ use Illuminate\Database\Eloquent\Relations\Relation;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Support\Stringable;
|
||||
use ReflectionClass;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
@@ -54,6 +56,7 @@ class MakePageCommand extends Command
|
||||
use CanAskForRelatedResource;
|
||||
use CanAskForResource;
|
||||
use CanAskForSchema;
|
||||
use CanAskForViewLocation;
|
||||
use CanCheckFileGenerationFlags;
|
||||
use CanManipulateFiles;
|
||||
use HasCluster;
|
||||
@@ -74,6 +77,8 @@ class MakePageCommand extends Command
|
||||
|
||||
protected ?string $view = null;
|
||||
|
||||
protected ?string $viewPath = null;
|
||||
|
||||
protected bool $hasResource;
|
||||
|
||||
/**
|
||||
@@ -349,12 +354,21 @@ class MakePageCommand extends Command
|
||||
$this->fqn = $this->pagesNamespace . '\\' . $this->fqnEnd;
|
||||
|
||||
if ((! $this->hasResource) || ($this->resourcePageType === ResourcePage::class)) {
|
||||
$this->view = str($this->fqn)
|
||||
->replaceFirst('App\\', '')
|
||||
->replace('\\', '/')
|
||||
->explode('/')
|
||||
->map(Str::kebab(...))
|
||||
->implode('.');
|
||||
[
|
||||
$this->view,
|
||||
$this->viewPath,
|
||||
] = $this->askForViewLocation(
|
||||
str($this->fqn)
|
||||
->whenContains(
|
||||
'Filament\\',
|
||||
fn (Stringable $fqn) => $fqn->after('Filament\\')->prepend('Filament\\'),
|
||||
fn (Stringable $fqn) => $fqn->replaceFirst('App\\', ''),
|
||||
)
|
||||
->replace('\\', '/')
|
||||
->explode('/')
|
||||
->map(Str::kebab(...))
|
||||
->implode('.'),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -635,17 +649,10 @@ class MakePageCommand extends Command
|
||||
return;
|
||||
}
|
||||
|
||||
$path = resource_path(
|
||||
(string) str($this->view)
|
||||
->replace('.', '/')
|
||||
->prepend('views/')
|
||||
->append('.blade.php'),
|
||||
);
|
||||
|
||||
if (! $this->option('force') && $this->checkForCollision($path)) {
|
||||
if (! $this->option('force') && $this->checkForCollision($this->viewPath)) {
|
||||
throw new InvalidCommandOutput;
|
||||
}
|
||||
|
||||
$this->copyStubToApp('PageView', $path);
|
||||
$this->copyStubToApp('PageView', $this->viewPath);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace Filament\Support;
|
||||
|
||||
class CliManager
|
||||
{
|
||||
/**
|
||||
* @var array<string, array{path: string, viewNamespace: ?string}>
|
||||
*/
|
||||
protected array $componentLocations = [];
|
||||
|
||||
public function registerComponentLocation(string $path, string $namespace, ?string $viewNamespace): void
|
||||
{
|
||||
$this->componentLocations[$namespace] = [
|
||||
'path' => $path,
|
||||
'viewNamespace' => $viewNamespace,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, array{path: string, viewNamespace: ?string}>
|
||||
*/
|
||||
public function getComponentLocations(): array
|
||||
{
|
||||
return $this->componentLocations;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
namespace Filament\Support\Commands\Concerns;
|
||||
|
||||
use Filament\Support\Facades\FilamentCli;
|
||||
|
||||
use function Laravel\Prompts\select;
|
||||
|
||||
trait CanAskForComponentLocation
|
||||
{
|
||||
/**
|
||||
* @return array{
|
||||
* 0: string,
|
||||
* 1: string,
|
||||
* 3: ?string,
|
||||
* }
|
||||
*/
|
||||
protected function askForComponentLocation(string $path, string $question = 'Where would you like to create the component?'): array
|
||||
{
|
||||
$pathNamespace = (string) str($path)->replace('/', '\\');
|
||||
|
||||
$locations = FilamentCli::getComponentLocations();
|
||||
|
||||
if (blank($locations)) {
|
||||
return [
|
||||
"App\\{$pathNamespace}",
|
||||
app_path($path),
|
||||
null,
|
||||
];
|
||||
}
|
||||
|
||||
$options = [
|
||||
null => "App\\{$pathNamespace}",
|
||||
...array_map(
|
||||
fn (string $namespace): string => "{$namespace}\\{$pathNamespace}",
|
||||
array_combine(
|
||||
array_keys($locations),
|
||||
array_keys($locations),
|
||||
),
|
||||
),
|
||||
];
|
||||
|
||||
$namespace = select(
|
||||
label: $question,
|
||||
options: $options,
|
||||
);
|
||||
|
||||
if (blank($namespace)) {
|
||||
return [
|
||||
"App\\{$pathNamespace}",
|
||||
app_path($path),
|
||||
'',
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
"{$namespace}\\{$pathNamespace}",
|
||||
$locations[$namespace]['path'] . '/' . $path,
|
||||
$locations[$namespace]['viewNamespace'] ?? null,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
namespace Filament\Support\Commands\Concerns;
|
||||
|
||||
use Illuminate\Contracts\View\Factory;
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
use function Laravel\Prompts\select;
|
||||
|
||||
trait CanAskForViewLocation
|
||||
{
|
||||
/**
|
||||
* @return array{
|
||||
* 0: string,
|
||||
* 1: string,
|
||||
* }
|
||||
*/
|
||||
protected function askForViewLocation(string $view, string $question = 'Where would you like to create the Blade view?', ?string $defaultNamespace = null): array
|
||||
{
|
||||
$viewFactory = app(Factory::class);
|
||||
|
||||
$paths = [];
|
||||
|
||||
if (str(Arr::first(config('view.paths')))->startsWith(base_path())) {
|
||||
$paths[''] = Arr::first(config('view.paths'));
|
||||
}
|
||||
|
||||
foreach ($viewFactory->getFinder()->getHints() as $namespace => $hintPaths) {
|
||||
foreach ($hintPaths as $path) {
|
||||
if (! str($path)->startsWith(base_path())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (str($path)->startsWith(base_path('vendor'))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($path === resource_path('views/vendor/livewire')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$paths[$namespace] = $path;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$options = array_map(
|
||||
fn (string $path): string => (string) str($path)->after(base_path()),
|
||||
$paths,
|
||||
);
|
||||
|
||||
$namespace = ($defaultNamespace !== null) && array_key_exists($defaultNamespace, $paths)
|
||||
? $defaultNamespace
|
||||
: (count($options) > 1
|
||||
? select(
|
||||
label: $question,
|
||||
options: $options,
|
||||
)
|
||||
: array_key_first($options));
|
||||
|
||||
if (blank($namespace)) {
|
||||
return [
|
||||
$view,
|
||||
resource_path(
|
||||
(string) str($view)
|
||||
->replace('.', '/')
|
||||
->prepend('views/')
|
||||
->append('.blade.php'),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
"{$namespace}::{$view}",
|
||||
(string) str($view)
|
||||
->replace('.', '/')
|
||||
->prepend("{$paths[$namespace]}/")
|
||||
->append('.blade.php'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -31,6 +31,10 @@ abstract class ClassGenerator implements FileGenerator
|
||||
$class->setExtends($extends);
|
||||
}
|
||||
|
||||
foreach ($this->getImplements() as $implement) {
|
||||
$class->addImplement($implement);
|
||||
}
|
||||
|
||||
$this->addTraitsToClass($class);
|
||||
$this->addPropertiesToClass($class);
|
||||
$this->addMethodsToClass($class);
|
||||
@@ -59,6 +63,14 @@ abstract class ClassGenerator implements FileGenerator
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<class-string>
|
||||
*/
|
||||
public function getImplements(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param class-string $name
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace Filament\Support\Facades;
|
||||
|
||||
use Filament\Support\CliManager;
|
||||
use Illuminate\Support\Facades\Facade;
|
||||
|
||||
/**
|
||||
* @method static void registerComponentLocation(string $directory, string $namespace, ?string $viewNamespace = null)
|
||||
* @method static array<string, string> getComponentLocations()
|
||||
*
|
||||
* @see CliManager
|
||||
*/
|
||||
class FilamentCli extends Facade
|
||||
{
|
||||
protected static function getFacadeAccessor(): string
|
||||
{
|
||||
return CliManager::class;
|
||||
}
|
||||
}
|
||||
@@ -69,6 +69,11 @@ class SupportServiceProvider extends PackageServiceProvider
|
||||
fn () => new AssetManager,
|
||||
);
|
||||
|
||||
$this->app->singleton(
|
||||
CliManager::class,
|
||||
fn () => new CliManager,
|
||||
);
|
||||
|
||||
$this->app->scoped(
|
||||
ScopedComponentManager::class,
|
||||
fn () => $this->app->make(ComponentManager::class)->clone(),
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
<?php
|
||||
|
||||
namespace Filament\Tables\Commands\FileGenerators;
|
||||
|
||||
use Filament\Support\Commands\FileGenerators\ClassGenerator;
|
||||
use Filament\Support\Components\Contracts\HasEmbeddedView;
|
||||
use Filament\Tables\Columns\Column;
|
||||
use Nette\PhpGenerator\ClassType;
|
||||
use Nette\PhpGenerator\Method;
|
||||
use Nette\PhpGenerator\Property;
|
||||
|
||||
class ColumnClassGenerator extends ClassGenerator
|
||||
{
|
||||
final public function __construct(
|
||||
protected string $fqn,
|
||||
protected bool $hasEmbeddedView,
|
||||
protected ?string $view,
|
||||
) {}
|
||||
|
||||
public function getNamespace(): string
|
||||
{
|
||||
return $this->extractNamespace($this->getFqn());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string>
|
||||
*/
|
||||
public function getImports(): array
|
||||
{
|
||||
return [
|
||||
$this->getExtends(),
|
||||
...($this->hasEmbeddedView() ? [HasEmbeddedView::class] : []),
|
||||
];
|
||||
}
|
||||
|
||||
public function getBasename(): string
|
||||
{
|
||||
return class_basename($this->getFqn());
|
||||
}
|
||||
|
||||
public function getExtends(): string
|
||||
{
|
||||
return Column::class;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<class-string>
|
||||
*/
|
||||
public function getImplements(): array
|
||||
{
|
||||
return [
|
||||
...($this->hasEmbeddedView() ? [HasEmbeddedView::class] : []),
|
||||
];
|
||||
}
|
||||
|
||||
protected function addPropertiesToClass(ClassType $class): void
|
||||
{
|
||||
$this->addViewPropertyToClass($class);
|
||||
}
|
||||
|
||||
protected function addMethodsToClass(ClassType $class): void
|
||||
{
|
||||
$this->addToEmbeddedHtmlMethodToClass($class);
|
||||
}
|
||||
|
||||
protected function addViewPropertyToClass(ClassType $class): void
|
||||
{
|
||||
if ($this->hasEmbeddedView()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$property = $class->addProperty('view', $this->getView())
|
||||
->setProtected()
|
||||
->setType('string');
|
||||
$this->configureViewProperty($property);
|
||||
}
|
||||
|
||||
protected function configureViewProperty(Property $property): void {}
|
||||
|
||||
protected function addToEmbeddedHtmlMethodToClass(ClassType $class): void
|
||||
{
|
||||
if (! $this->hasEmbeddedView()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$method = $class->addMethod('toEmbeddedHtml')
|
||||
->setPublic()
|
||||
->setReturnType('string')
|
||||
->setBody(<<<'PHP'
|
||||
ob_start(); ?>
|
||||
|
||||
<div>
|
||||
<?= e($this->getState()) ?>
|
||||
</div>
|
||||
|
||||
<?php return ob_get_clean();
|
||||
PHP);
|
||||
|
||||
$this->configureToEmbeddedHtmlMethod($method);
|
||||
}
|
||||
|
||||
protected function configureToEmbeddedHtmlMethod(Method $method): void {}
|
||||
|
||||
public function getFqn(): string
|
||||
{
|
||||
return $this->fqn;
|
||||
}
|
||||
|
||||
public function hasEmbeddedView(): bool
|
||||
{
|
||||
return $this->hasEmbeddedView;
|
||||
}
|
||||
|
||||
public function getView(): ?string
|
||||
{
|
||||
return $this->view;
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,18 @@
|
||||
|
||||
namespace Filament\Tables\Commands;
|
||||
|
||||
use Filament\Support\Commands\Concerns\CanAskForComponentLocation;
|
||||
use Filament\Support\Commands\Concerns\CanAskForViewLocation;
|
||||
use Filament\Support\Commands\Concerns\CanManipulateFiles;
|
||||
use Filament\Support\Commands\Exceptions\InvalidCommandOutput;
|
||||
use Filament\Tables\Commands\FileGenerators\ColumnClassGenerator;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Str;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
|
||||
use function Laravel\Prompts\confirm;
|
||||
use function Laravel\Prompts\text;
|
||||
|
||||
#[AsCommand(name: 'make:filament-table-column', aliases: [
|
||||
@@ -16,11 +23,25 @@ use function Laravel\Prompts\text;
|
||||
])]
|
||||
class MakeColumnCommand extends Command
|
||||
{
|
||||
use CanAskForComponentLocation;
|
||||
use CanAskForViewLocation;
|
||||
use CanManipulateFiles;
|
||||
|
||||
protected $description = 'Create a new table column class and cell view';
|
||||
|
||||
protected $signature = 'make:filament-table-column {name?} {--F|force}';
|
||||
protected $name = 'make:filament-table-column';
|
||||
|
||||
protected string $fqnEnd;
|
||||
|
||||
protected string $fqn;
|
||||
|
||||
protected string $path;
|
||||
|
||||
protected bool $hasEmbeddedView;
|
||||
|
||||
protected ?string $view = null;
|
||||
|
||||
protected ?string $viewPath = null;
|
||||
|
||||
/**
|
||||
* @var array<string>
|
||||
@@ -31,59 +52,141 @@ class MakeColumnCommand extends Command
|
||||
'make:table-column',
|
||||
];
|
||||
|
||||
/**
|
||||
* @return array<InputArgument>
|
||||
*/
|
||||
protected function getArguments(): array
|
||||
{
|
||||
return [
|
||||
new InputArgument(
|
||||
name: 'name',
|
||||
mode: InputArgument::OPTIONAL,
|
||||
description: 'The name of the column to generate, optionally prefixed with directories',
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<InputOption>
|
||||
*/
|
||||
protected function getOptions(): array
|
||||
{
|
||||
return [
|
||||
new InputOption(
|
||||
name: 'embedded-view',
|
||||
shortcut: 'E',
|
||||
mode: InputOption::VALUE_NONE,
|
||||
description: 'Define embedded HTML inside the class instead of using a separate Blade view file',
|
||||
),
|
||||
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
|
||||
{
|
||||
$column = (string) str($this->argument('name') ?? text(
|
||||
try {
|
||||
$this->configureFqnEnd();
|
||||
$this->configureHasEmbeddedView();
|
||||
|
||||
$this->configureLocation();
|
||||
|
||||
$this->createColumn();
|
||||
$this->createView();
|
||||
} catch (InvalidCommandOutput) {
|
||||
return static::FAILURE;
|
||||
}
|
||||
|
||||
$this->components->info("Filament table column [{$this->fqn}] created successfully.");
|
||||
|
||||
return static::SUCCESS;
|
||||
}
|
||||
|
||||
protected function configureFqnEnd(): void
|
||||
{
|
||||
$this->fqnEnd = (string) str($this->argument('name') ?? text(
|
||||
label: 'What is the column name?',
|
||||
placeholder: 'StatusSwitcher',
|
||||
placeholder: 'StatusSwitcherColumn',
|
||||
required: true,
|
||||
))
|
||||
->trim('/')
|
||||
->trim('\\')
|
||||
->trim(' ')
|
||||
->studly()
|
||||
->replace('/', '\\');
|
||||
$columnClass = (string) str($column)->classBasename();
|
||||
$columnNamespace = str($column)->contains('\\') ?
|
||||
(string) str($column)->beforeLast('\\') :
|
||||
'';
|
||||
}
|
||||
|
||||
$view = str($column)
|
||||
->prepend('filament\\tables\\columns\\')
|
||||
->explode('\\')
|
||||
->map(fn ($segment) => Str::kebab($segment))
|
||||
->implode('.');
|
||||
|
||||
$path = app_path(
|
||||
(string) str($column)
|
||||
->prepend('Filament\\Tables\\Columns\\')
|
||||
->replace('\\', '/')
|
||||
->append('.php'),
|
||||
);
|
||||
$viewPath = resource_path(
|
||||
(string) str($view)
|
||||
->replace('.', '/')
|
||||
->prepend('views/')
|
||||
->append('.blade.php'),
|
||||
protected function configureHasEmbeddedView(): void
|
||||
{
|
||||
$this->hasEmbeddedView = $this->option('embedded-view') || confirm(
|
||||
label: 'Do you want to embed the HTML of the view in the column class?',
|
||||
default: false,
|
||||
hint: 'Defining the HTML of the column in the class instead of in a Blade view file improves the performance of the column, but doesn\'t allow you to use Blade syntax.',
|
||||
);
|
||||
}
|
||||
|
||||
if (! $this->option('force') && $this->checkForCollision([
|
||||
protected function configureLocation(): void
|
||||
{
|
||||
[
|
||||
$namespace,
|
||||
$path,
|
||||
])) {
|
||||
return static::INVALID;
|
||||
$viewNamespace,
|
||||
] = $this->askForComponentLocation(
|
||||
path: 'Filament/Tables/Columns',
|
||||
question: 'Where would you like to create the column?',
|
||||
);
|
||||
|
||||
$this->fqn = "{$namespace}\\{$this->fqnEnd}";
|
||||
$this->path = (string) str("{$path}\\{$this->fqnEnd}.php")
|
||||
->replace('\\', '/')
|
||||
->replace('//', '/');
|
||||
|
||||
if ($this->hasEmbeddedView) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->copyStubToApp('Column', $path, [
|
||||
'class' => $columnClass,
|
||||
'namespace' => 'App\\Filament\\Tables\\Columns' . ($columnNamespace !== '' ? "\\{$columnNamespace}" : ''),
|
||||
'view' => $view,
|
||||
]);
|
||||
[
|
||||
$this->view,
|
||||
$this->viewPath,
|
||||
] = $this->askForViewLocation(
|
||||
str($this->fqn)
|
||||
->afterLast('Filament\\Tables\\Columns')
|
||||
->prepend('Filament\\Tables\\Columns')
|
||||
->replace('\\', '/')
|
||||
->explode('/')
|
||||
->map(Str::kebab(...))
|
||||
->implode('.'),
|
||||
defaultNamespace: $viewNamespace,
|
||||
);
|
||||
}
|
||||
|
||||
if (! $this->fileExists($viewPath)) {
|
||||
$this->copyStubToApp('ColumnView', $viewPath);
|
||||
protected function createColumn(): void
|
||||
{
|
||||
if (! $this->option('force') && $this->checkForCollision($this->path)) {
|
||||
throw new InvalidCommandOutput;
|
||||
}
|
||||
|
||||
$this->components->info("Filament table column [{$path}] created successfully.");
|
||||
$this->writeFile($this->path, app(ColumnClassGenerator::class, [
|
||||
'fqn' => $this->fqn,
|
||||
'hasEmbeddedView' => $this->hasEmbeddedView,
|
||||
'view' => $this->view,
|
||||
]));
|
||||
}
|
||||
|
||||
return static::SUCCESS;
|
||||
protected function createView(): void
|
||||
{
|
||||
if (blank($this->view)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (! $this->option('force') && $this->checkForCollision($this->viewPath)) {
|
||||
throw new InvalidCommandOutput;
|
||||
}
|
||||
|
||||
$this->copyStubToApp('ColumnView', $this->viewPath);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user