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

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

准备工作

安装 PHP-CS-Fixerblade-formatter

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

创建修复器 BladeFixer.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

<?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);

测试示例

╰─ 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

相关链接

原文连接

本作品采用《CC 协议》,转载必须注明作者和本文链接
No practice, no gain in one's wit. 我的 Gitub
《L01 基础入门》
我们将带你从零开发一个项目并部署到线上,本课程教授 Web 开发中专业、实用的技能,如 Git 工作流、Laravel Mix 前端工作流等。
《G01 Go 实战入门》
从零开始带你一步步开发一个 Go 博客项目,让你在最短的时间内学会使用 Go 进行编码。项目结构很大程度上参考了 Laravel。
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!
未填写
文章
59
粉丝
132
喜欢
992
收藏
1349
排名:45
访问:15.5 万
私信
所有博文
社区赞助商