2023/11/26

PHP Quality

要兼顧 PHP 開發品質,在 commit 之前可以用一些套件檢測,不管是否有符合 coding convention 或是有沒有語法上的錯誤,都可以預防性的監測。

composer require --dev phpstan/phpstan squizlabs/php_codesniffer friendsofphp/php-cs-fixer phpmd/phpmd phpunit/phpunit brianium/paratest

搭配 Makefile 可以減少每次執行要打的指令,此配置是針對 Laravel,可依照不同環境內容變動。

Makefile
fix-code-format:
	vendor/bin/php-cs-fixer fix --verbose

code-style-check:
	vendor/bin/phpstan analyse
	vendor/bin/phpcs --standard=PSR12 app tests
	vendor/bin/php-cs-fixer fix --dry-run
	vendor/bin/phpmd app,config,database,routes,tests text phpmd.xml

test: code-style-check
	vendor/bin/paratest --colors --processes 4 --runner=WrapperRunner
phpstan.neon
parameters:
  paths:
    - app
  excludePaths:
    - app/Http/Middleware/Authenticate.php
    - app/Providers/RouteServiceProvider.php

  level: 5

  parallel:
    processTimeout: 60.0
    maximumNumberOfProcesses: 32
    minimumNumberOfJobsPerProcess: 2

  checkMissingIterableValueType: false
.php-cs-fixer.php
<?php

use PhpCsFixer\Config;
use PhpCsFixer\Finder;

$rules = [
    'array_syntax' => ['syntax' => 'short'],
    'binary_operator_spaces' => ['default' => 'single_space'],
    'blank_line_after_namespace' => true,
    'blank_line_after_opening_tag' => true,
    'blank_line_before_statement' => true,
    'cast_spaces' => true,
    'class_definition' => true,
    'declare_equal_normalize' => true,
    'elseif' => true,
    'encoding' => true,
    'full_opening_tag' => true,
    'function_declaration' => true,
    'type_declaration_spaces' => true,
    'lowercase_cast' => true,
    'heredoc_to_nowdoc' => true,
    'include' => true,
    'indentation_type' => true,
    'lowercase_keywords' => true,
    'magic_constant_casing' => true,
    'method_argument_space' => true,
    'phpdoc_separation' => false,
    'native_function_casing' => true,
    'no_alias_functions' => true,
    'no_blank_lines_after_class_opening' => true,
    'no_blank_lines_after_phpdoc' => true,
    'multiline_whitespace_before_semicolons' => ['strategy' => 'no_multi_line'],
    'no_closing_tag' => true,
    'no_empty_phpdoc' => true,
    'no_empty_statement' => true,
    'no_leading_import_slash' => true,
    'no_leading_namespace_whitespace' => true,
    '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_spaces_around_offset' => ['positions' => ['inside']],
    'spaces_inside_parentheses' => true,
    'no_trailing_comma_in_singleline' => true,
    'no_trailing_whitespace' => true,
    'no_trailing_whitespace_in_comment' => true,
    'no_unneeded_control_parentheses' => true,
    'no_unreachable_default_argument_value' => true,
    'no_unused_imports' => 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_class_elements' => true,
    'ordered_imports' => ['sort_algorithm' => 'alpha'],
    'phpdoc_indent' => true,
    'phpdoc_line_span' => ['const' => 'multi', 'method' => 'multi', 'property' => 'single'],
    'phpdoc_no_access' => true,
    'phpdoc_no_useless_inheritdoc' => true,
    'phpdoc_order' => true,
    'phpdoc_scalar' => true,
    'phpdoc_single_line_var_spacing' => true,
    'phpdoc_to_comment' => true,
    'phpdoc_trim' => true,
    'phpdoc_no_alias_tag' => ['replacements' => ['type' => 'var']],
    'phpdoc_types' => true,
    'phpdoc_var_without_name' => true,
    'no_mixed_echo_print' => ['use' => 'echo'],
    'self_accessor' => true,
    'short_scalar_cast' => true,
    'simplified_null_return' => true,
    'single_blank_line_at_eof' => true,
    'blank_lines_before_namespace' => true,
    'single_class_element_per_statement' => true,
    'single_import_per_statement' => true,
    'single_line_after_imports' => true,
    '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' => true,
    'whitespace_after_comma_in_array' => true,
    'concat_space' => ['spacing' => 'one'],
    'single_space_around_construct' => true,
    'control_structure_braces' => true,
    'braces_position' => true,
    'control_structure_continuation_position' => true,
    'declare_parentheses' => true,
    'statement_indentation' => true,
];

$excludes = [
    'bootstrap/cache',
    'storage',
    'vendor',
    'node_modules',
];

$finder = PhpCsFixer\Finder::create()
                           ->in(__DIR__)
                           ->exclude($excludes)
                           ->notName('*.xml')
                           ->notName('*.yml');

$config = new PhpCsFixer\Config();
$config->setRiskyAllowed(true)
       ->setIndent('    ')
       ->setLineEnding("\n")
       ->setRules($rules)
       ->setUsingCache(true)
       ->setFinder($finder);

return $config;
phpmd.xml
<?xml version="1.0" encoding="UTF-8"?>
<ruleset name="Netask rule set"
    xmlns="http://pmd.sf.net/ruleset/1.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://pmd.sf.net/ruleset/1.0.0 http://pmd.sf.net/ruleset_xml_schema.xsd"
    xsi:noNamespaceSchemaLocation="http://pmd.sf.net/ruleset_xml_schema.xsd">
<description>Netask code style rule set
</description>
<rule ref="rulesets/codesize.xml/CyclomaticComplexity"/>
<rule ref="rulesets/codesize.xml/NPathComplexity"/>
<rule ref="rulesets/codesize.xml/ExcessiveMethodLength"/>
<rule ref="rulesets/codesize.xml/ExcessiveClassLength"/>
<rule ref="rulesets/codesize.xml/ExcessiveParameterList"/>
<rule ref="rulesets/codesize.xml/ExcessivePublicCount">
    <properties>
        <property name="minimum" value="30"/>
    </properties>
</rule>
<rule ref="rulesets/codesize.xml/TooManyFields">
    <properties>
        <property name="maxfields" value="20"/>
    </properties>
</rule>
<rule ref="rulesets/codesize.xml/TooManyMethods"/>
<rule ref="rulesets/codesize.xml/ExcessiveClassComplexity">
    <properties>
        <property name="maximum" value="30"/>
    </properties>
</rule>
<rule ref="rulesets/controversial.xml"/>
<rule ref="rulesets/design.xml"/>
<rule ref="rulesets/naming.xml/ShortVariable"/>
<rule ref="rulesets/naming.xml/LongVariable">
    <properties>
        <property name="maximum" value="30"/>
    </properties>
</rule>
<rule ref="rulesets/naming.xml/ShortMethodName">
    <properties>
        <property name="minimum" value="2"/>
    </properties>
</rule>
<rule ref="rulesets/naming.xml/ConstructorWithNameAsEnclosingClass"/>
<rule ref="rulesets/naming.xml/ConstantNamingConventions"/>
<rule ref="rulesets/naming.xml/BooleanGetMethodName"/>
<rule ref="rulesets/unusedcode.xml/UnusedPrivateField" />
<rule ref="rulesets/unusedcode.xml/UnusedLocalVariable" />

<exclude-pattern>app/Console/Kernel.php</exclude-pattern>
<exclude-pattern>app/Services/Service.php</exclude-pattern>
<exclude-pattern>tests/TestCase.php</exclude-pattern>
</ruleset>
phpunit.xml
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="./vendor/phpunit/phpunit/phpunit.xsd"
         bootstrap="vendor/autoload.php"
         colors="true"
>
    <testsuites>
        <testsuite name="Unit">
            <directory suffix="Test.php">./tests/Unit</directory>
        </testsuite>
        <testsuite name="Feature">
            <directory suffix="Test.php">./tests/Feature</directory>
        </testsuite>
    </testsuites>
    <coverage processUncoveredFiles="true">
        <include>
            <directory suffix=".php">./app</directory>
        </include>
    </coverage>
    <php>
        <server name="APP_ENV" value="testing"/>
        <server name="BCRYPT_ROUNDS" value="4"/>
        <server name="CACHE_DRIVER" value="array"/>
        <!-- <server name="DB_CONNECTION" value="sqlite"/> -->
        <!-- <server name="DB_DATABASE" value=":memory:"/> -->
        <server name="MAIL_MAILER" value="array"/>
        <server name="QUEUE_CONNECTION" value="sync"/>
        <server name="SESSION_DRIVER" value="array"/>
        <server name="TELESCOPE_ENABLED" value="false"/>
    </php>
</phpunit>