Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions src/Console/Commands/FieldtypesList.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php

namespace Statamic\Console\Commands;

use Illuminate\Console\Command;
use Statamic\Console\RunsInPlease;
use Statamic\Fields\FieldtypeInspector;
use Symfony\Component\Console\Output\OutputInterface;

class FieldtypesList extends Command
{
use RunsInPlease;

protected $signature = 'statamic:fieldtypes:list {--json : Output the information as JSON}';

protected $description = 'List the installed fieldtypes';

public function handle(FieldtypeInspector $inspector): int
{
$data = $inspector->all();

if ($this->option('json')) {
$this->output->writeln(
json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR),
OutputInterface::OUTPUT_RAW
);

return self::SUCCESS;
}

$this->components->info('Statamic '.$data['statamic_version']);

$this->table(['Handle', 'Title', 'Selectable', 'Class'], array_map(fn ($fieldtype) => [
$fieldtype['handle'],
$fieldtype['title'],
$fieldtype['selectable'] ? 'Yes' : 'No',
$fieldtype['class'],
], $data['fieldtypes']));

return self::SUCCESS;
}
}
54 changes: 54 additions & 0 deletions src/Console/Commands/FieldtypesShow.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php

namespace Statamic\Console\Commands;

use Illuminate\Console\Command;
use Statamic\Console\RunsInPlease;
use Statamic\Exceptions\FieldtypeNotFoundException;
use Statamic\Fields\FieldtypeInspector;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Yaml\Yaml;

class FieldtypesShow extends Command
{
use RunsInPlease;

protected $signature = 'statamic:fieldtypes:show
{handle : The registered fieldtype handle}
{--json : Output the information as JSON}';

protected $description = 'Show configuration definitions for an installed fieldtype';

protected $help = <<<'HELP'
Defaults describe fieldtype definitions, not an existing field's effective configuration or blueprint overrides.

Missing or unresolved options may come from the editor or project context; defaults are not an exhaustive list of choices.
HELP;

public function handle(FieldtypeInspector $inspector): int
{
try {
$data = $inspector->show($this->argument('handle'));
} catch (FieldtypeNotFoundException $exception) {
if ($this->option('json')) {
$this->output->writeln(
json_encode(['error' => $exception->getMessage()], JSON_THROW_ON_ERROR),
OutputInterface::OUTPUT_RAW
);
} else {
$this->components->error($exception->getMessage());
}

return self::FAILURE;
}

$this->output->writeln(
$this->option('json')
? json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR)
: Yaml::dump($data, 10, 2),
OutputInterface::OUTPUT_RAW
);

return self::SUCCESS;
}
}
111 changes: 111 additions & 0 deletions src/Fields/FieldtypeInspector.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
<?php

namespace Statamic\Fields;

use BackedEnum;
use Illuminate\Support\Collection;
use Statamic\Statamic;

/**
* @internal
*/
class FieldtypeInspector
{
private array $unresolved = [];

public function __construct(private FieldtypeRepository $fieldtypes)
{
}

public function all(): array
{
return [
'statamic_version' => Statamic::version(),
'fieldtypes' => $this->fieldtypes->classes()
->sortKeys()
->map(fn ($class, $handle) => $this->metadata($handle, app($class)))
->values()->all(),
];
}

public function show(string $handle): array
{
$fieldtype = $this->fieldtypes->find($handle);

$this->unresolved = [];
$config = $this->definitions($fieldtype->configFields(), ['config']);
$common = $this->definitions(Field::commonFieldOptions(), ['common']);

return [
'statamic_version' => Statamic::version(),
'fieldtype' => $this->metadata($handle, $fieldtype),
'config' => $config,
'common' => $common,
'unresolved' => $this->unresolved,
];
}

private function metadata(string $handle, Fieldtype $fieldtype): array
{
return [
'handle' => $handle,
'class' => $fieldtype::class,
'title' => $fieldtype->title(),
'selectable' => $fieldtype->selectable(),
'selectable_in_forms' => $fieldtype->selectableInForms(),
'localizable' => $fieldtype->localizable(),
'validatable' => $fieldtype->validatable(),
'defaultable' => $fieldtype->defaultable(),
];
}

private function definitions(Fields $fields, array $path): array
{
return $fields->all()
->map(fn (Field $field, $handle) => $this->definition($field, [...$path, $handle]))
->all();
}

private function definition(Field $field, array $path): array
{
$definition = $field->config();
$editorType = $definition['type'] ?? null;
unset($definition['type']);

$definition = ['editor_type' => $editorType] + $definition;

if (
! array_key_exists('options', $definition)
&& in_array($editorType, ['bard_buttons_setting', 'markdown_buttons_setting'], true)
) {
$this->unresolved[] = ['path' => [...$path, 'options'], 'source' => 'editor'];
}

return $this->normalize($definition, $path);
}

private function normalize(mixed $value, array $path): mixed
{
if ($value instanceof BackedEnum) {
return $value->value;
}

if ($value instanceof Collection) {
$value = $value->all();
}

if (is_array($value)) {
return collect($value)
->map(fn ($item, $key) => $this->normalize($item, [...$path, $key]))
->all();
}

if (is_object($value) || is_resource($value)) {
$this->unresolved[] = ['path' => $path, 'source' => 'php', 'php_type' => get_debug_type($value)];

return null;
}

return $value;
}
}
2 changes: 2 additions & 0 deletions src/Providers/ConsoleServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ class ConsoleServiceProvider extends ServiceProvider
Commands\AssetsGeneratePresets::class,
Commands\AssetsMeta::class,
Commands\AssetsMetaClean::class,
Commands\FieldtypesList::class,
Commands\FieldtypesShow::class,
Commands\GlideClear::class,
Commands\Install::class,
Commands\InstallCollaboration::class,
Expand Down
62 changes: 62 additions & 0 deletions tests/Console/Commands/FieldtypesListTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<?php

namespace Tests\Console\Commands;

use Illuminate\Support\Facades\Artisan;
use PHPUnit\Framework\Attributes\Test;
use Statamic\Fields\Fieldtype;
use Tests\TestCase;

class FieldtypesListTest extends TestCase
{
#[Test]
public function it_lists_registered_fieldtypes_without_evaluating_their_configuration()
{
app('statamic.fieldtypes')->put('test_listed', ListedFieldtype::class);
app('statamic.fieldtypes')->put('test_unconfigured', UnconfiguredFieldtype::class);

$this->assertSame(0, Artisan::call('statamic:fieldtypes:list', ['--json' => true]));

$data = json_decode(Artisan::output(), true, 512, JSON_THROW_ON_ERROR);
$handles = array_column($data['fieldtypes'], 'handle');

$this->assertSame('3.0.0-testing', $data['statamic_version']);
$this->assertContains('bard', $handles);
$this->assertContains('markdown', $handles);
$this->assertContains('test_listed', $handles);
$this->assertContains('test_unconfigured', $handles);

$sorted = $handles;
sort($sorted);
$this->assertSame($sorted, $handles);

$fieldtype = collect($data['fieldtypes'])->firstWhere('handle', 'test_listed');
$this->assertSame(ListedFieldtype::class, $fieldtype['class']);
$this->assertSame('<fg=red>Listed</>', $fieldtype['title']);
$this->assertFalse($fieldtype['selectable']);
}

#[Test]
public function it_can_list_fieldtypes_without_json()
{
$this->artisan('statamic:fieldtypes:list')
->expectsOutputToContain('bard')
->expectsOutputToContain('markdown')
->assertSuccessful();
}
}

class ListedFieldtype extends Fieldtype
{
protected static $title = '<fg=red>Listed</>';

protected $selectable = false;
}

class UnconfiguredFieldtype extends Fieldtype
{
protected function configFieldItems(): array
{
throw new \RuntimeException('Listing must not evaluate configuration.');
}
}
Loading
Loading