Laravel専用の自動整形ツール Laravel Pintを試してみました

Laravel専用の自動整形ツール Laravel Pintを試してみました

2022年6月23日、Laravel専用の自動整形ツールLaravel Pintがプレリリースされたため、Laravel Pintを試してみました。

確認環境

  • PHP 8.0
  • Laravel 9
  • Laravel Pint v0.1.17

インストール

composerコマンドでインストール可能です。

$ composer require laravel/pint --dev

Laravel Pinをインストールするには、PHP 8.0以上が必要です。

自動整形

自動整形実行

$ vendor/bin/pint -v

実行結果画面は以下のような形式になっています。

実行結果

中略

修正内容の詳細結果

ドライラン

$ vendor/bin/pint --test -v

ドライランではルールチェックのみを行い、ファイルの修正は行われません。

プリセット

resources/presets/laravel.php にLaravelのプリセット設定があります。PHP-CS-Fixerのルールの詳細はドキュメントから確認することができます。プリセット情報を見るとPint独自のフォーマットルールも用意されていることも分かります。

<?php

use App\Factories\ConfigurationFactory;

return ConfigurationFactory::preset([
    'array_indentation' => true,
    'array_syntax' => ['syntax' => 'short'],
    'binary_operator_spaces' => [
        'default' => 'single_space',
        'operators' => ['=>' => null],
    ],
    'blank_line_after_namespace' => true,
    'blank_line_after_opening_tag' => true,
    'blank_line_before_statement' => [
        'statements' => ['return'],
    ],
    'braces' => true,
    'cast_spaces' => true,
    'class_attributes_separation' => [
        'elements' => [
            'const' => 'one',
            'method' => 'one',
            'property' => 'one',
            'trait_import' => 'none',
        ],
    ],
    'class_definition' => [
        'multi_line_extends_each_single_line' => true,
        'single_item_single_line' => true,
        'single_line' => true,
    ],
    'clean_namespace' => true,
    'compact_nullable_typehint' => true,
    'concat_space' => [
        'spacing' => 'none',
    ],
    'constant_case' => ['case' => 'lower'],
    'declare_equal_normalize' => true,
    'elseif' => true,
    'encoding' => true,
    'full_opening_tag' => true,
    'fully_qualified_strict_types' => true,
    'function_declaration' => true,
    'function_typehint_space' => true,
    'general_phpdoc_tag_rename' => true,
    'heredoc_to_nowdoc' => true,
    'include' => true,
    'increment_style' => ['style' => 'post'],
    'indentation_type' => true,
    'integer_literal_case' => true,
    'lambda_not_used_import' => true,
    'linebreak_after_opening_tag' => true,
    'line_ending' => true,
    'lowercase_cast' => true,
    'lowercase_keywords' => true,
    'lowercase_static_reference' => true,
    'magic_method_casing' => true,
    'magic_constant_casing' => true,
    'method_argument_space' => [
        'on_multiline' => 'ignore',
    ],
    'multiline_whitespace_before_semicolons' => [
        'strategy' => 'no_multi_line',
    ],
    'native_function_casing' => true,
    'native_function_type_declaration_casing' => true,
    'no_alias_functions' => true,
    'no_alias_language_construct_call' => true,
    'no_alternative_syntax' => true,
    'no_binary_string' => true,
    'no_blank_lines_after_class_opening' => true,
    'no_blank_lines_after_phpdoc' => true,
    'no_closing_tag' => true,
    'no_empty_phpdoc' => true,
    'no_empty_statement' => true,
    'no_extra_blank_lines' => [
        'tokens' => [
            'extra',
            'throw',
            'use',
        ],
    ],
    'no_leading_import_slash' => true,
    'no_leading_namespace_whitespace' => true,
    'no_mixed_echo_print' => [
        'use' => 'echo',
    ],
    'no_multiline_whitespace_around_double_arrow' => true,
    'no_short_bool_cast' => true,
    'no_singleline_whitespace_before_semicolons' => true,
    'no_spaces_after_function_name' => true,
    'no_space_around_double_colon' => true,
    'no_spaces_around_offset' => [
        'positions' => ['inside', 'outside'],
    ],
    'no_spaces_inside_parenthesis' => true,
    'no_trailing_comma_in_list_call' => true,
    'no_trailing_comma_in_singleline_array' => true,
    'no_trailing_whitespace' => true,
    'no_trailing_whitespace_in_comment' => true,
    'no_unneeded_control_parentheses' => [
        'statements' => ['break', 'clone', 'continue', 'echo_print', 'return', 'switch_case', 'yield'],
    ],
    'no_unneeded_curly_braces' => true,
    'no_unset_cast' => true,
    'no_unused_imports' => true,
    'no_unreachable_default_argument_value' => true,
    'no_useless_return' => true,
    'no_whitespace_before_comma_in_array' => true,
    'no_whitespace_in_blank_line' => true,
    'normalize_index_brace' => true,
    'not_operator_with_successor_space' => true,
    'object_operator_without_whitespace' => true,
    'ordered_imports' => ['sort_algorithm' => 'alpha'],
    'psr_autoloading' => false,
    'phpdoc_indent' => true,
    'phpdoc_inline_tag_normalizer' => true,
    'phpdoc_no_access' => true,
    'phpdoc_no_package' => true,
    'phpdoc_no_useless_inheritdoc' => true,
    'phpdoc_scalar' => true,
    'phpdoc_single_line_var_spacing' => true,
    'phpdoc_summary' => false,
    'phpdoc_to_comment' => false,
    'phpdoc_tag_type' => true,
    'phpdoc_trim' => true,
    'phpdoc_types' => true,
    'phpdoc_var_without_name' => true,
    'return_type_declaration' => ['space_before' => 'none'],
    'self_accessor' => true,
    'short_scalar_cast' => true,
    'simplified_null_return' => false,
    'single_blank_line_at_eof' => true,
    'single_blank_line_before_namespace' => true,
    'single_class_element_per_statement' => [
        'elements' => ['const', 'property'],
    ],
    'single_import_per_statement' => true,
    'single_line_after_imports' => true,
    'single_line_comment_style' => [
        'comment_types' => ['hash'],
    ],
    'single_quote' => true,
    'space_after_semicolon' => true,
    'standardize_not_equals' => true,
    'switch_case_semicolon_to_colon' => true,
    'switch_case_space' => true,
    'ternary_operator_spaces' => true,
    'trailing_comma_in_multiline' => ['elements' => ['arrays']],
    'trim_array_spaces' => true,
    'unary_operator_spaces' => true,
    'visibility_required' => [
        'elements' => ['method', 'property'],
    ],
    'whitespace_after_comma_in_array' => true,

    // Laravel
    'Laravel/laravel_phpdoc_alignment' => true,
    'Laravel/laravel_phpdoc_order' => true,
    'Laravel/laravel_phpdoc_separation' => true,
]);

独自フォーマットルール

v0.1.17の段階では、以下の3つのルールが用意されているようです。

  • Laravel/laravel_phpdoc_alignment
  • Laravel/laravel_phpdoc_order
  • Laravel/laravel_phpdoc_separation

どのルールもPHPDocに関連するもののようです。

詳細実装は、app/Fixers ディレクトリ以下にあります。

PHP-CS-Fixerとの違い

細かい設定が不要

適用ファイルや適用ルールなどの細かい設定は、Pintが予め行ってくれているため、簡単にプロジェクトに導入ができます。

独自フォーマットルール

現段階では、独自フォーマットルールはPHPDocに関連するものしかありませんが、今後は増えていくことは予測されるため、Laravel特有のエッジケースなどは、吸収されて行くのではないかと思います。また、Laravelのバージョンアップの補助などもサポートしてくれるようになる可能性もあります。

依存ゼロ

Laravel Pint is an opinionated PHP code style fixer for minimalists. Pint is built on top of PHP-CS-Fixer and makes it simple to ensure that your code style stays clean and consistent.

GitHubより

READMEでは、PHP-CS-Fixerをベースとした依存ゼロの自動整形ツールであると紹介されており、依存ゼロとは、Twitterでインストール時にPHPのみに依存し、他には依存しないとのことと説明されています。

https://twitter.com/enunomaduro/status/1539627908125401088?s=20&t=9utaQjMboiMMUszqBn0RTg

Laravel Pintは、PHP-CS-Fixerをベースに作成されていますが、決してPHP-CS-Fixerが依存ゼロでない訳ではありません。PHP-CS-Fixerのモジュール版(Phar)もありますし、Composer bin pluginを使用すれば、プロジェクトのcomposer.jsonを汚すことなくPHP-CS-Fixerをインストール可能です。

まとめ

現段階では、ベースとなったPHP-CS-Fixerと大きな差はなく、切り替えるメリットは少ないかと思われます。

プレリリースされたばかりなので、次は正式リリースされたときに改めて確認したいと思います。最近の動向からするに正式リリースは恐らく、Laravel 10または11のリリース時と同時に正式版へすこし無理やりでも格上げされるのではないかと思います。

付録

私は、下記のような設定でPHP-CS-Fixerを利用しています。しかし、コードスタイルへの強いこだわりはありません。どのコードスタイルを適用するかよりもプロジェクトないでコードスタイルが統一されているかの方が重要だと思っています。例えインデントがTABでもGo言語のように統一されていれば、問題ないかと思います。逆に単一ファイル内でさえもインデントが統一されていないと結構ストレスを感じてしまいます。

<?php declare(strict_types=1);

$finder = PhpCsFixer\Finder::create()
    ->in([
        __DIR__ . '/app',
        __DIR__ . '/config',
        __DIR__ . '/database/factories',
        __DIR__ . '/database/seeders',
        __DIR__ . '/routes',
        __DIR__ . '/tests',
    ]);

$config = new PhpCsFixer\Config();

return $config
    ->setRiskyAllowed(true)
    ->setRules([
        '@Symfony' => true,
        '@PhpCsFixer:risky' => true,
        '@PHP80Migration' => true,
        '@PHP80Migration:risky' => true,
        '@PSR12' => true,
        '@PHPUnit84Migration:risky' => true,

        // Alias
        'ereg_to_preg' => true,
        'mb_str_functions' => true,

        // Array Notation

        // Basic

        // Casing

        // Cast Notation

        // Class Notation
        'class_attributes_separation' => [
            'elements' => ['method' => 'one']
        ],
        'self_static_accessor' => true,
        'no_null_property_initialization' => true,

        // Class Usage

        // Comment
        'multiline_comment_opening_closing' => true,

        // Constant Notation

        // Control Structure
        'no_superfluous_elseif' => true,
        'no_useless_else' => true,
        'yoda_style' => false,

        // Doctrine Annotation

        // Function Notation
        'static_lambda' => true,
        'single_line_throw' => false,
        'use_arrow_functions' => false,

        // import
        'global_namespace_import' => [
            'import_classes' => true,
            'import_constants' => true,
            'import_functions' => true,
        ],
        'ordered_imports' => [
            'sort_algorithm' => 'alpha',
            'imports_order' => [
                'class',
                'function',
                'const'
            ],
        ],

        // Language Construct

        // List Notation

        // Namespace Notation

        // Naming

        // Operator
        'concat_space' => [
            'spacing' => 'one'
        ],
        'new_with_braces' => true,

        // PHP Tag

        // PHPUnit
        'php_unit_test_case_static_method_calls' => [
            'call_type' => 'this'
        ],
        'php_unit_strict' => false,

        // PHPDoc
        'align_multiline_comment' => true,
        'general_phpdoc_tag_rename' => [
            'replacements' => [
                'inheritDocs' => 'inheritDoc',
            ]
        ],
        'no_superfluous_phpdoc_tags' => [
            'allow_mixed' => true,
            'allow_unused_params' => true,
        ],
        'phpdoc_no_empty_return' => true,
        'phpdoc_order' => true,
        'phpdoc_order_by_value' => true,
        'phpdoc_var_annotation_correct_order' => true,
        'phpdoc_summary' => false,

        // Return Notation
        'no_useless_return' => true,
        'return_assignment' => true,

        // Semicolon
        'multiline_whitespace_before_semicolons' => [
            'strategy' => 'no_multi_line',
        ],

        // Strict
        'declare_strict_types' => false,

        // String Notation
        'escape_implicit_backslashes' => true,
        'explicit_string_variable' => true,
        'heredoc_to_nowdoc' => true,
        'simple_to_complex_string_variable' => true,

        // Whitespace
        'array_indentation' => true,
        'method_chaining_indentation' => true,
    ])
    ->setFinder($finder);

参考サイト