当前位置: 首页 > news >正文

php-cs-fixer 集成 blade-formatter 来格式化 blade 模板

# php-cs-fixer 集成 blade-formatter 来格式化 blade 模板

## 准备工作

安装 [PHP-CS-Fixer](https://github.com/PHP-CS-Fixer/PHP-CS-Fixer) 和 [blade-formatter](https://github.com/shufo/blade-formatter)

```shell
composer require --dev friendsofphp/php-cs-fixer
npm install -g blade-formatter
```

## 创建修复器 [BladeFixer.php](https://github.com/guanguans/laravel-skeleton/blob/main/app/Support/PhpCsFixer/Fixer/BladeFixer.php)

```php
<?php

/** @noinspection MissingParentCallInspection */
/** @noinspection PhpConstantNamingConventionInspection */
/** @noinspection PhpDeprecationInspection */
/** @noinspection PhpInternalEntityUsedInspection */
/** @noinspection PhpMissingParentCallCommonInspection */
/** @noinspection SensitiveParameterInspection */

declare(strict_types=1);

/**
* Copyright (c) 2021-2025 guanguans<ityaozm@gmail.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*
* @see https://github.com/guanguans/laravel-skeleton
*/

namespace App\Support\PhpCsFixer\Fixer;

use PhpCsFixer\AbstractFixer;
use PhpCsFixer\FileReader;
use PhpCsFixer\FileRemoval;
use PhpCsFixer\Fixer\ConfigurableFixerInterface;
use PhpCsFixer\Fixer\ConfigurableFixerTrait;
use PhpCsFixer\FixerConfiguration\FixerConfigurationResolver;
use PhpCsFixer\FixerConfiguration\FixerConfigurationResolverInterface;
use PhpCsFixer\FixerConfiguration\FixerOptionBuilder;
use PhpCsFixer\FixerDefinition\CodeSample;
use PhpCsFixer\FixerDefinition\FixerDefinition;
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
use PhpCsFixer\Tokenizer\Tokens;
use PhpCsFixer\Utils;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Input\ArgvInput;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\ConsoleOutput;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Process\Exception\ProcessFailedException;
use Symfony\Component\Process\ExecutableFinder;
use Symfony\Component\Process\Process;

/**
* @see https://github.com/shufo/blade-formatter
*
* @property array{
* command: array,
* options: array,
* cwd: ?string,
* env: ?array,
* input: ?string,
* timeout: ?float
* } $configuration
*
* @method void configureIO(InputInterface *$input*, OutputInterface *$output*)
*/
final class BladeFixer extends AbstractFixer implements ConfigurableFixerInterface
{
use ConfigurableFixerTrait;
public const string *COMMAND* = 'command';
public const string *OPTIONS* = 'options';
public const string *CWD* = 'cwd';
public const string *ENV* = 'env';
public const string *INPUT* = 'input';
public const string *TIMEOUT* = 'timeout';

#[\Override]
public function getDefinition(): FixerDefinitionInterface
{
return new FixerDefinition(
$summary = \sprintf('Format a [%s] file.', $this->getShortHeadlineName()),
[new CodeSample($summary)]
);
}

public static function name(): string
{
return (new self)->getName();
}

#[\Override]
public function getName(): string
{
return \sprintf('User/%s', $this->getShortName());
}

public function getShortHeadlineName(): string
{
return str($this->getShortName())->headline()->toString();
}

public function getShortName(): string
{
return parent::getName();
}

#[\Override]
public function isRisky(): bool
{
return true;
}

#[\Override]
public function getPriority(): int
{
return \*PHP_INT_MAX*;
}

/**
* @param \PhpCsFixer\Tokenizer\Tokens<\PhpCsFixer\Tokenizer\Token> *$tokens*
*/
#[\Override]
public function isCandidate(Tokens *$tokens*): bool
{
return *$tokens*->count() === 1 && *$tokens*[0]->isGivenKind(\*T_INLINE_HTML*);
}

#[\Override]
public function supports(\SplFileInfo *$file*): bool
{
return str_ends_with(*$file*->getBasename(), '.blade.php');
}

protected function createConfigurationDefinition(): FixerConfigurationResolverInterface
{
return new FixerConfigurationResolver([
(new FixerOptionBuilder(self::*COMMAND*, 'The blade-formatter command to run.'))
->setAllowedTypes(['string', 'array'])
->setDefault('blade-formatter')
->setNormalizer(static fn (OptionsResolver *$optionsResolver*, array|string *$value*): array => array_map(
static fn (string *$value*): string => str_contains(*$value*, \*DIRECTORY_SEPARATOR*)
? *$value*
: (new ExecutableFinder)->find(*$value*, *$value*),
(array) *$value*,
))
->getOption(),
(new FixerOptionBuilder(self::*OPTIONS*, 'The options to pass to the command.'))
->setAllowedTypes(['array'])
->setDefault([])
->setNormalizer(static fn (OptionsResolver *$optionsResolver*, array *$value*): array => collect(*$value*)->reduce(
static function (array *$options*, mixed *$value*, int|string *$key*): array {
\is_string(*$key*) and str_starts_with(*$key*, '-') and *$options*[] = *$key*;
*$options*[] = *$value*;

return *$options*;
},
[]
))
->getOption(),
(new FixerOptionBuilder(self::*CWD*, 'The working directory or null to use the working dir of the current PHP process.'))
->setAllowedTypes(['string', 'null'])
->setDefault(null)
->getOption(),
(new FixerOptionBuilder(self::*ENV*, 'The environment variables or null to use the same environment as the current PHP process.'))
->setAllowedTypes(['array', 'null'])
->setDefault(null)
->getOption(),
(new FixerOptionBuilder(self::*INPUT*, 'The input as stream resource, scalar or \Traversable, or null for no input.'))
->setAllowedTypes(['string', 'null'])
->setDefault(null)
->getOption(),
(new FixerOptionBuilder(self::*TIMEOUT*, 'The timeout in seconds or null to disable.'))
->setAllowedTypes(['float', 'int', 'null'])
->setDefault(10)
->getOption(),
]);
}

/**
* @param \PhpCsFixer\Tokenizer\Tokens<\PhpCsFixer\Tokenizer\Token> *$tokens*
*
* @throws \Throwable
*/
#[\Override]
protected function applyFix(\SplFileInfo *$file*, Tokens *$tokens*): void
{
$process = new Process(
command: [
...$this->configuration[self::*COMMAND*],
$finalPath = $this->finalFile(*$file*, *$tokens*),
'--write',
...$this->configuration[self::*OPTIONS*],
],
cwd: $this->configuration[self::*CWD*],
env: $this->configuration[self::*ENV*],
input: $this->configuration[self::*INPUT*],
timeout: $this->configuration[self::*TIMEOUT*],
);
$process->run();
$this->debugProcess($process);

if (!$process->isSuccessful()) {
throw new ProcessFailedException($process);
}

*$tokens*->setCode(FileReader::createSingleton()->read($finalPath));
}

/**
* @noinspection GlobalVariableUsageInspection
*
* @param \PhpCsFixer\Tokenizer\Tokens<\PhpCsFixer\Tokenizer\Token> *$tokens*
*/
private function finalFile(\SplFileInfo *$file*, Tokens *$tokens*): string
{
$finalFile = (string) *$file*;

if (\in_array('--dry-run', $_SERVER['argv'] ?? [], true)) {
file_put_contents($finalFile = $this->createTemporaryFile(), *$tokens*->generateCode());
}

return $finalFile;
}

private function createTemporaryFile(): string
{
static $temporaryFile;

if ($temporaryFile) {
return $temporaryFile;
}

$temporaryFile = tempnam($tempDir = sys_get_temp_dir(), "{$this->getShortName()}_");

if (!$temporaryFile) {
throw new \RuntimeException("The temporary file could not be created in the temporary directory [$tempDir].");
}

(new FileRemoval)->observe($temporaryFile);

return $temporaryFile;
}

/**
* @noinspection DuplicatedCode
*/
private function debugProcess(Process *$process*): void
{
if (!($symfonyStyle = $this->createSymfonyStyle())->isDebug()) {
return;
}

$symfonyStyle->title("Process debugging information for [{$this->getName()}]");
$symfonyStyle->warning([
\sprintf('Command Line: %s', *$process*->getCommandLine()),
\sprintf('Exit Code: %s', Utils::toString(*$process*->getExitCode())),
\sprintf('Exit Code Text: %s', Utils::toString(*$process*->getExitCodeText())),
\sprintf('Output: %s', *$process*->getOutput()),
\sprintf('Error Output: %s', *$process*->getErrorOutput()),
\sprintf('Working Directory: %s', Utils::toString(*$process*->getWorkingDirectory())),
\sprintf('Env: %s', Utils::toString(*$process*->getEnv())),
\sprintf('Input: %s', Utils::toString(*$process*->getInput())),
\sprintf('Timeout: %s', Utils::toString(*$process*->getTimeout())),
]);
}

private function createSymfonyStyle(): SymfonyStyle
{
$argvInput = new ArgvInput;
$consoleOutput = new ConsoleOutput;

// to configure all -v, -vv, -vvv options without memory-lock to Application run() arguments
(fn () => $this->configureIO($argvInput, $consoleOutput))->call(new Application);

return new SymfonyStyle($argvInput, $consoleOutput);
}
}
```

## 配置修复器 [BladeFixer.php](https://github.com/guanguans/laravel-skeleton/blob/main/app/Support/PhpCsFixer/Fixer/BladeFixer.php)

```php
<?php

declare(strict_types=1);

/**
* Copyright (c) 2021-2025 guanguans<ityaozm@gmail.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*
* @see https://github.com/guanguans/laravel-skeleton
*/

use App\Support\PhpCsFixer\Fixer\BladeFixer;
use PhpCsFixer\Config;

return (new Config)
// ... 其他配置
->registerCustomFixers([
new BladeFixer,
])
// ... 其他配置
->setRules([
// ... 其他规则配置

// // 自定义 BladeFixer 规则配置
// BladeFixer::name() => [
// BladeFixer::COMMAND => ['path/to/node', 'path/to/blade-formatter'],
// BladeFixer::OPTIONS => [
// '--config' => 'path/to/.bladeformatterrc',
// '--indent-size' => 2,
// // ...
// ],
// ],
// 默认 BladeFixer 规则配置
BladeFixer::name() => true,

// ... 其他规则配置
])
// ... 其他配置
->setRiskyAllowed(true);
```

## 测试示例

```shell
╰─ vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.php --using-cache=no --diff --dry-run --ansi -vv tests.blade.php ─╯
PHP CS Fixer 3.84.0 Alexander by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 8.3.23
Running analysis on 7 cores with 10 files per process.
Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!
Loaded config ergebnis (PHP 8.3) (future mode) from ".php-cs-fixer.php".
Paths from configuration file have been overridden by paths provided as command arguments.
1/1 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%

1) tests.blade.php (User/blade)
---------- begin diff ----------
--- /Users/yaozm/Documents/wwwroot/laravel-skeleton/tests.blade.php
+++ /Users/yaozm/Documents/wwwroot/laravel-skeleton/tests.blade.php
@@ -10,16 +10,16 @@
</div>
<div class="pf-users-branch">
<ul class="pf-users-branch__list">
- @foreach($users as $user)
+ @foreach ($users as $user)
<li>
<img src="{{ asset('img/frontend/icon/branch-arrow.svg') }}" alt="branch_arrow">
- {{ link_to_route("frontend.users.user.show",$users["name"],$users['_id']) }}
+ {{ link_to_route('frontend.users.user.show', $users['name'], $users['_id']) }}
</li>
@endforeach
</ul>
<div class="pf-users-branch__btn">
@can('create', App\Models\User::class)
- {!! link_to_route("frontend.users.user.create",__('users.create'),[1,2,3],['class' => 'btn']) !!}
+ {!! link_to_route('frontend.users.user.create', __('users.create'), [1, 2, 3], ['class' => 'btn']) !!}
@endcan
</div>
</div>

----------- end diff -----------


Found 1 of 1 files that can be fixed in 0.697 seconds, 38.00 MB memory used
```

## 相关链接

* [https://github.com/PHP-CS-Fixer/PHP-CS-Fixer](https://github.com/PHP-CS-Fixer/PHP-CS-Fixer)
* [https://github.com/shufo/blade-formatter](https://github.com/shufo/blade-formatter)
* [https://github.com/guanguans/laravel-skeleton/tree/main/app/Support/PhpCsFixer](https://github.com/guanguans/laravel-skeleton/tree/main/app/Support/PhpCsFixer)

## 原文连接

* [https://github.com/guanguans/guanguans.github.io/issues/59](https://github.com/guanguans/guanguans.github.io/issues/59)

http://www.vanclimg.com/news/3006.html

相关文章:

  • 2025/7/30 模拟赛总结
  • 73、把姓名转为一列
  • CRMEB会员电商系统集群部署实战:腾讯云镜像优化指南
  • 跨平台文件对比工具中的字体与布局兼容性问题深度解析
  • 5.2 表示程序性能
  • C语言基础-练习:九九乘法表
  • P12247 跳舞机(平衡树+动态规划)
  • 跨域问题处理
  • 15个好用的网络抓包工具,开箱即用
  • linux使用非交互的方式修改指定用户密码
  • 模拟赛day4题解
  • 银河麒麟通过 docker 离线安装 opengauss 数据库(单节点部署)
  • npm 发布工具包
  • 解析 RS485 总线:从技术内核到终端电阻的可靠性密码
  • dify之类工作流的理解
  • Unity Mask遮罩失效问题
  • 详细讲解了Linux定时任务调度的两种任务调度的机制和语法:crond周期任务调度、at一次性任务调度 - 实践
  • suse系统上创建用户和组
  • 新增SSH免密设置
  • 莫比乌斯
  • 图像生成-条件概率与边缘概率-10 - jack
  • GBase8a使用like %%进行模糊查询时,返回结果不符合预期
  • GBase8a查询decimal类型的字段时,返回结果集不符合预期
  • GBase8a使用sql找出表中有乱码的数据
  • NumPy的reshape自动计算(-1表示​​自动计算该维度的大小)
  • GBase8a安装部署集群时,提示gbase密码不正确,已确认密码无误
  • CF2096H Wonderful XOR Problem 题解
  • GBase8a安装部署集群时,报错Invalid or offline nodes:ip
  • Feign框架中一处编码不合理导致的异常
  • nginx 文件服务器