Skip to content

Commit 02cf9d7

Browse files
committed
feat: add support for user-defined presets via composer
This allows users to create and distribute custom presets through composer packages, enabling better code style sharing across projects. This also adds a new `preset:list` command to show all available presets.
1 parent 4efb0b2 commit 02cf9d7

File tree

9 files changed

+331
-31
lines changed

9 files changed

+331
-31
lines changed

app/Commands/PresetListCommand.php

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php
2+
3+
namespace App\Commands;
4+
5+
use App\Services\PresetManifest;
6+
use LaravelZero\Framework\Commands\Command;
7+
use Symfony\Component\Console\Attribute\AsCommand;
8+
9+
#[AsCommand('preset:list', 'List all available presets')]
10+
class PresetListCommand extends Command
11+
{
12+
public function handle(PresetManifest $presetManifest): int
13+
{
14+
$presets = $presetManifest->names();
15+
16+
if ($presets === []) {
17+
$this->components->warn('No presets found.');
18+
19+
return self::SUCCESS;
20+
}
21+
22+
$this->newLine();
23+
$this->components->twoColumnDetail(
24+
'<fg=green;options=bold>Preset</>',
25+
'<fg=yellow;options=bold>Path</>',
26+
);
27+
28+
foreach ($presets as $preset) {
29+
$path = $presetManifest->path($preset);
30+
$presets[$preset] = $path;
31+
$this->components->twoColumnDetail($preset, $path);
32+
}
33+
34+
return self::SUCCESS;
35+
}
36+
}

app/Factories/ConfigurationResolverFactory.php

Lines changed: 6 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,26 +4,14 @@
44

55
use App\Project;
66
use App\Repositories\ConfigurationJsonRepository;
7+
use App\Services\PresetManifest;
78
use ArrayIterator;
89
use PhpCsFixer\Config;
910
use PhpCsFixer\Console\ConfigurationResolver;
1011
use PhpCsFixer\ToolInfo;
1112

1213
class ConfigurationResolverFactory
1314
{
14-
/**
15-
* The list of available presets.
16-
*
17-
* @var array<int, string>
18-
*/
19-
public static $presets = [
20-
'laravel',
21-
'per',
22-
'psr12',
23-
'symfony',
24-
'empty',
25-
];
26-
2715
/**
2816
* Creates a new PHP CS Fixer Configuration Resolver instance
2917
* from the given input and output.
@@ -37,23 +25,20 @@ public static function fromIO($input, $output)
3725
$path = Project::paths($input);
3826

3927
$localConfiguration = resolve(ConfigurationJsonRepository::class);
28+
$presetManifest = resolve(PresetManifest::class);
4029

4130
$preset = $localConfiguration->preset();
4231

43-
if (! in_array($preset, static::$presets)) {
44-
abort(1, 'Preset not found.');
32+
if (! $presetManifest->has($preset)) {
33+
$availablePresets = implode(', ', $presetManifest->names());
34+
abort(1, "Preset '{$preset}' not found. Available presets: {$availablePresets}");
4535
}
4636

4737
$resolver = new ConfigurationResolver(
4838
new Config('default'),
4939
[
5040
'allow-risky' => 'yes',
51-
'config' => implode(DIRECTORY_SEPARATOR, [
52-
dirname(__DIR__, 2),
53-
'resources',
54-
'presets',
55-
sprintf('%s.php', $preset),
56-
]),
41+
'config' => $presetManifest->path($preset),
5742
'diff' => $output->isVerbose(),
5843
'dry-run' => $input->getOption('test') || $input->getOption('bail'),
5944
'path' => $path,

app/Output/SummaryOutput.php

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44

55
use App\Output\Concerns\InteractsWithSymbols;
66
use App\Project;
7+
use App\Services\PresetManifest;
78
use App\ValueObjects\Issue;
9+
use Illuminate\Support\Str;
810
use PhpCsFixer\Runner\Event\FileProcessed;
911

1012
use function Termwind\render;
@@ -15,17 +17,21 @@ class SummaryOutput
1517
use InteractsWithSymbols;
1618

1719
/**
18-
* The list of presets, in a human-readable format.
20+
* Get the list of presets in a human-readable format.
1921
*
20-
* @var array<string, string>
22+
* @return array<string, string>
2123
*/
22-
protected $presets = [
23-
'per' => 'PER',
24-
'psr12' => 'PSR 12',
25-
'laravel' => 'Laravel',
26-
'symfony' => 'Symfony',
27-
'empty' => 'Empty',
28-
];
24+
protected function getPresets(): array
25+
{
26+
$presetManifest = resolve(PresetManifest::class);
27+
$presets = [];
28+
29+
foreach ($presetManifest->names() as $preset) {
30+
$presets[$preset] = Str::headline($preset);
31+
}
32+
33+
return [...$presets, 'per' => 'PER', 'psr12' => 'PSR 12'];
34+
}
2935

3036
/**
3137
* Creates a new Summary Output instance.
@@ -63,7 +69,7 @@ public function handle($summary, $totalFiles)
6369
'totalFiles' => $totalFiles,
6470
'issues' => $issues,
6571
'testing' => $summary->isDryRun(),
66-
'preset' => $this->presets[$this->config->preset()],
72+
'preset' => $this->getPresets()[$this->config->preset()] ?? $this->config->preset(),
6773
]),
6874
);
6975

app/Providers/AppServiceProvider.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace App\Providers;
44

5+
use App\Services\PresetManifest;
56
use Illuminate\Support\ServiceProvider;
67
use PhpCsFixer\Error\ErrorsManager;
78
use Symfony\Component\EventDispatcher\EventDispatcher;
@@ -32,5 +33,13 @@ public function register()
3233
$this->app->singleton(EventDispatcher::class, function () {
3334
return new EventDispatcher;
3435
});
36+
37+
$this->app->singleton(PresetManifest::class, function ($app) {
38+
return new PresetManifest(
39+
$app->make('files'),
40+
$app->basePath(),
41+
$app->basePath('bootstrap/cache/pint_presets.php'),
42+
);
43+
});
3544
}
3645
}

app/Services/PresetManifest.php

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
<?php
2+
3+
namespace App\Services;
4+
5+
use Illuminate\Filesystem\Filesystem;
6+
use Illuminate\Support\Collection;
7+
8+
class PresetManifest
9+
{
10+
/** @var ?array<string, string> */
11+
protected ?array $manifest = null;
12+
13+
protected string $vendorPath;
14+
15+
public function __construct(
16+
protected Filesystem $files,
17+
protected string $basePath,
18+
protected string $manifestPath,
19+
) {
20+
$this->vendorPath = $this->basePath.'/vendor';
21+
}
22+
23+
/**
24+
* Get all available presets from packages.
25+
*
26+
* @return array<string, string> ['preset-name' => '/absolute/path/to/preset.php']
27+
*/
28+
public function presets(): array
29+
{
30+
return $this->manifest ??= $this->getManifest();
31+
}
32+
33+
/**
34+
* Check if a preset exists.
35+
*/
36+
public function has(string $preset): bool
37+
{
38+
return array_key_exists($preset, $this->presets());
39+
}
40+
41+
/**
42+
* Get the path for a specific preset.
43+
*/
44+
public function path(string $preset): ?string
45+
{
46+
return $this->presets()[$preset] ?? null;
47+
}
48+
49+
/**
50+
* Get all preset names.
51+
*
52+
* @return list<string>
53+
*/
54+
public function names(): array
55+
{
56+
return array_keys($this->presets());
57+
}
58+
59+
/**
60+
* Get the current preset manifest.
61+
*
62+
* @return array<string, string>
63+
*/
64+
protected function getManifest(): array
65+
{
66+
$path = $this->vendorPath.'/composer/installed.json';
67+
68+
if (
69+
! $this->files->exists($this->manifestPath) ||
70+
$this->files->lastModified($path) > $this->files->lastModified($this->manifestPath)
71+
) {
72+
return $this->build();
73+
}
74+
75+
return $this->files->getRequire($this->manifestPath);
76+
}
77+
78+
/**
79+
* Build the manifest and write it to disk.
80+
*
81+
* @return array<string, string>
82+
*/
83+
protected function build(): array
84+
{
85+
$packages = [];
86+
$installedPath = $this->vendorPath.'/composer/installed.json';
87+
$composerPath = $this->basePath.'/composer.json';
88+
89+
if ($this->files->exists($installedPath)) {
90+
$installed = json_decode($this->files->get($installedPath), true);
91+
$packages = $installed['packages'] ?? $installed;
92+
}
93+
94+
$presets = (new Collection($packages))
95+
->keyBy(fn ($package) => $this->vendorPath.'/'.$package['name'])
96+
->when($this->files->exists($composerPath), function ($presets) use ($composerPath) {
97+
$composer = json_decode($this->files->get($composerPath), true);
98+
99+
return $presets->put($this->basePath, $composer);
100+
})
101+
->map(fn ($package) => $package['extra']['laravel-pint']['presets'] ?? [])
102+
->flatMap(function (array $presets, string $basePath): array {
103+
foreach ($presets as $name => $relativePath) {
104+
$absolutePath = $basePath.'/'.$relativePath;
105+
106+
if ($this->files->exists($absolutePath)) {
107+
$presets[$name] = $absolutePath;
108+
} else {
109+
unset($presets[$name]);
110+
}
111+
}
112+
113+
return $presets;
114+
})
115+
->all();
116+
117+
$this->write($presets);
118+
119+
return $presets;
120+
}
121+
122+
/**
123+
* Write the given manifest array to disk.
124+
*
125+
* @param array<string, string> $manifest
126+
*/
127+
protected function write(array $manifest): void
128+
{
129+
$this->files->ensureDirectoryExists(dirname($this->manifestPath), 0755, true);
130+
$this->files->replace($this->manifestPath, '<?php return '.var_export($manifest, true).';');
131+
}
132+
}

bootstrap/cache/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
*
2+
!.gitignore

composer.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,17 @@
5858
"pestphp/pest-plugin": true
5959
}
6060
},
61+
"extra": {
62+
"laravel-pint": {
63+
"presets": {
64+
"laravel": "resources/presets/laravel.php",
65+
"per": "resources/presets/per.php",
66+
"psr12": "resources/presets/psr12.php",
67+
"symfony": "resources/presets/symfony.php",
68+
"empty": "resources/presets/empty.php"
69+
}
70+
}
71+
},
6172
"minimum-stability": "dev",
6273
"prefer-stable": true,
6374
"bin": ["builds/pint"]

tests/Feature/PresetDiscoveryTest.php

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
3+
use App\Services\PresetManifest;
4+
5+
it('can resolve preset paths', function () {
6+
$presetManifest = resolve(PresetManifest::class);
7+
8+
expect($presetManifest->has('laravel'))->toBeTrue();
9+
expect($presetManifest->path('laravel'))->toContain('resources/presets/laravel.php');
10+
11+
expect($presetManifest->has('nonexistent'))->toBeFalse();
12+
expect($presetManifest->path('nonexistent'))->toBeNull();
13+
});
14+
15+
it('can list available presets', function () {
16+
$this->artisan('preset:list')
17+
->expectsOutputToContain('laravel')
18+
->expectsOutputToContain('per')
19+
->expectsOutputToContain('psr12')
20+
->expectsOutputToContain('symfony')
21+
->expectsOutputToContain('empty')
22+
->assertSuccessful();
23+
});

0 commit comments

Comments
 (0)