Coding style,

Static code analysis and PHP

Outline

  • About me

  • What's Coding style?

  • PSR-2與PSR-12程式碼風格標準。

  • What's static code analysis?

    • PHPStan

    • Psalm

    • Phan

  • CI/CD examples

  • Laravel framework integration

About me

  • Peter
  • GitHub
  • Active open source contributor
  • An associate engineer
    • DevOps
    • Back-end
    • System Architecture Researching
    • Web Application Security
    • PHP, Python and JavaScript
  • Smart Grid Technology (2017~2021)
  • Database, Data platform architecture (2021~)

What's coding style?

AKA Programming style

PHP有Coding style嗎?

PHP有Coding style嗎?

Coding style

PSR-1 Overview

  • Files MUST use only <?php and <?= tags.

  • Files MUST use only UTF-8 without BOM for PHP code.

  • Files SHOULD either declare symbols (classes, functions, constants, etc.) or cause side-effects (e.g. generate output, change .ini settings, etc.) but SHOULD NOT do both.

  • Namespaces and classes MUST follow an "autoloading" PSR: [PSR-0, PSR-4].

  • Class names MUST be declared in StudlyCaps.

  • Class constants MUST be declared in all upper case with underscore separators.

  • Method names MUST be declared in camelCase.

     

PSR-2 Overview(Deprecated)

  • Code MUST follow a "coding style guide" PSR [PSR-1].

  • Code MUST use 4 spaces for indenting, not tabs.

  • There MUST NOT be a hard limit on line length; the soft limit MUST be 120 characters; lines SHOULD be 80 characters or less.

  • There MUST be one blank line after the namespace declaration, and there MUST be one blank line after the block of use declarations.

  • Opening braces for classes MUST go on the next line, and closing braces MUST go on the next line after the body.

  • Opening braces for methods MUST go on the next line, and closing braces MUST go on the next line after the body.

  • Visibility MUST be declared on all properties and methods; abstract and final MUST be declared before the visibility; static MUST be declared after the visibility.

  • Control structure keywords MUST have one space after them; method and function calls MUST NOT.

  • Opening braces for control structures MUST go on the same line, and closing braces MUST go on the next line after the body.

  • Opening parentheses for control structures MUST NOT have a space after them, and closing parentheses for control structures MUST NOT have a space before.

  • This specification extends, expands and replaces PSR-2, the coding style guide and requires adherence to PSR-1, the basic coding standard.

規則太多要檢查,有沒有檢查工具?

PHP_CodeSniffer

  • curl -OL https://squizlabs.github.io/PHP_CodeSniffer/phpcs.phar
  • chmod +x phpcs.phar
  • mv phpcs.phar phpcs
  • phpcs --help
  • phpcs --standard=PSR2 src/ tests/
  • curl -OL https://squizlabs.github.io/PHP_CodeSniffer/phpcbf.phar
  • chmod +x phpcbf.phar
  • mv phpcbf.phar phpcbf
  • phpcbf --help
  • phpcbf --standard=PSR2 src/ tests/

phpcs --standard=PSR2

FILE: ...n-source-contributions/localized/src/Validation/LtValidation.php
----------------------------------------------------------------------
FOUND 1 ERROR AFFECTING 1 LINE
----------------------------------------------------------------------
 31 | ERROR | [x] Use single instead of double quotes for simple
    |       |     strings.
----------------------------------------------------------------------
PHPCBF CAN FIX THE 1 MARKED SNIFF VIOLATIONS AUTOMATICALLY
----------------------------------------------------------------------
FILE: ...is/build/open-source-contributions/localized/tests/bootstrap.php
----------------------------------------------------------------------
FOUND 1 ERROR AFFECTING 1 LINE
----------------------------------------------------------------------
 15 | ERROR | [x] Use single instead of double quotes for simple
    |       |     strings.
----------------------------------------------------------------------
PHPCBF CAN FIX THE 1 MARKED SNIFF VIOLATIONS AUTOMATICALLY
----------------------------------------------------------------------
FILE: ...n-source-contributions/localized/src/Validation/BrValidation.php
----------------------------------------------------------------------
FOUND 2 ERRORS AFFECTING 1 LINE
----------------------------------------------------------------------
 196 | ERROR | [x] Use single instead of double quotes for simple
     |       |     strings.
 196 | ERROR | [x] Use single instead of double quotes for simple
     |       |     strings.
----------------------------------------------------------------------
PHPCBF CAN FIX THE 2 MARKED SNIFF VIOLATIONS AUTOMATICALLY
----------------------------------------------------------------------

phpcs --standard=PSR2

phpcbf --standard=PSR2

phpcs.xml

<?xml version="1.0"?>
<ruleset name="Coding Standard">
    <arg name="basepath" value="."/>
    <arg name="colors"/>
    <arg value="sp"/>

    <config name="ignore_warnings_on_exit" value="1"/>

    <file>./src</file>
    <file>./tests</file>

    <rule ref="PSR2"></rule>
    <!-- <rule ref="PSR12"></rule> -->

    <rule ref="Squiz.Commenting.ClassComment">
        <exclude name="Squiz.Commenting.ClassComment.TagNotAllowed"/>
        <type>warning</type>
        <exclude-pattern>*/tests/</exclude-pattern>
    </rule>
    <rule ref="Squiz.Commenting.ClassComment.Missing">
        <type>warning</type>
    </rule>
    <rule ref="Squiz.Commenting.FunctionComment.Missing">
        <type>warning</type>
        <exclude-pattern>*/config/</exclude-pattern>
    </rule>
    <rule ref="Squiz.Commenting.FunctionComment.MissingParamTag">
        <type>warning</type>
    </rule>
    <rule ref="Squiz.Commenting.FunctionComment.MissingParamComment">
        <type>warning</type>
    </rule>
    <rule ref="Squiz.Commenting.FunctionComment.ParamCommentNotCapital">
        <type>warning</type>
    </rule>

    <rule ref="Generic.Metrics.CyclomaticComplexity">
        <properties>
            <property name="absoluteComplexity" value="50"/>
        </properties>
    </rule>
    <rule ref="Generic.Metrics.NestingLevel">
        <properties>
            <property name="nestingLevel" value="2"/>
            <property name="absoluteNestingLevel" value="4"/>
        </properties>
    </rule>
</ruleset>

PHP-CS-Fixer

  • curl -OL https://cs.symfony.com/download/php-cs-fixer-v2.phar

  • php php-cs-fixer-v2.phar fix --dry-run --format=txt --verbose --diff --diff-format=udiff --config=.cs.php

  • curl -OL https://cs.symfony.com/download/php-cs-fixer-v3.phar

  • php php-cs-fixer-v3.phar fix --dry-run --format=txt --verbose --diff --diff-format=udiff --config=.cs.php

.cs.php

<?php

return PhpCsFixer\Config::create()
  ->setUsingCache(false)
  ->setRiskyAllowed(true)
  //->setCacheFile(__DIR__ . '/.php_cs.cache')
  ->setRules([
    '@PSR1' => true,
    '@PSR2' => true,
    '@Symfony' => true,
    'psr4' => true,
    'yoda_style' => false,
    'array_syntax' => ['syntax' => 'short'],
    'list_syntax' => ['syntax' => 'short'],
    'concat_space' => ['spacing' => 'one'],
    'cast_spaces' => ['space' => 'none'],
    'compact_nullable_typehint' => true,
    'increment_style' => ['style' => 'post'],
    'declare_equal_normalize' => ['space' => 'single'],
    'no_short_echo_tag' => true,
    'protected_to_private' => false,
    'phpdoc_align' => false,
    'phpdoc_add_missing_param_annotation' => ['only_untyped' => false],
    'phpdoc_order' => true, // psr-5
    'phpdoc_no_empty_return' => false,
    'align_multiline_comment' => true, // psr-5
    'general_phpdoc_annotation_remove' => [
      'annotations' => [
        'author',
        'package',
      ],
    ],
  ])
  ->setFinder(PhpCsFixer\Finder::create()
    ->in(__DIR__ . '/src')
    ->in(__DIR__ . '/tests')
    ->name('*.php')
    ->ignoreDotFiles(true)
    ->ignoreVCS(true));

What's static code analysis?

Static Code Analysis

  • It's the analysis of computer software that is performed without actually executing programs.

  • Dynamic code analysis is the analysis of computer software that is performed by executing programs.

    • ​Unit tests, integration tests, system tests and acceptance tests use dynamic testing.

Static Code Analysis for PHP

  • Psalm

  • PHPStan

  • Phan→The PHP Father recommended

Installation

Installation

  • composer require phpstan/phpstan:0.* --dev

  • composer require vimeo/psalm:4.* --dev

  • composer require phan/phan:5.* --dev

Standard Checks

  • there are no syntax errors;

  • all the classes, methods, functions and constants exist;

  • the variables exist;

  • the hints in PHPDoc correspond to reality;

  • there are no arguments or variables unused.

Avoid copy-caste code errors and careless

Data type checks

  • Most analyzers allow to configure the level of strictness of checking and imitate strict_types:

    • they check that String or Boolean aren’t passed to this function.

Union types

  • Most analyzers allow to configure the level of strictness of checking and imitate strict_types:

    • they check that String or Boolean aren’t passed to this function.

/**  
 * @var string|int|bool $yes_or_no
 */
function isYes($yes_or_no) :bool 
{
     if (is_numeric($yes_or_no)) {
         return $yes_or_no > 0;
     } else {
         return strtoupper($yes_or_no) == 'YES';
     }
}

False type

  • Most analyzers allow to configure the level of strictness of checking and imitate strict_types:

    • they check that String or Boolean aren’t passed to this function.

/** @return int|bool */
 function fwrite(...) {
        …
 }

False type Error

<?php

/** @return resource|bool */
function open_file() {
    $fp = fopen('./composer.json', 'r');
    if($fp === false) {
        return false;
    }

    return fwrite($fp, "some string");
}
lee@lee-VirtualBox:~/phpstan-example$ vendor/bin/phpstan analyse ./false_type.php --level=max -c phpstan.neon --no-progress --ansi
 ------ --------------------------------------------------------------------------------------------
  Line   false_type.php
 ------ --------------------------------------------------------------------------------------------
  4      Function open_file() never returns resource so it can be removed from the return typehint.
  10     Function open_file() should return bool|resource but returns int|false.
 ------ --------------------------------------------------------------------------------------------

False type Error Fix

<?php

/** @return int|false */
function open_file() {
    $fp = fopen('./composer.json', 'r');
    if($fp === false) {
        return false;
    }

    return fwrite($fp, "some string");
}
lee@lee-VirtualBox:~/phpstan-example$ vendor/bin/phpstan analyse ./false_type.php \
	--level=max -c phpstan.neon --no-progress --ansi

 [OK] No errors

Array shapes

<?php

/** @return array */
function array_func(array $arr) {
    return $arr;
}
lee@lee-VirtualBox:~/phpstan-example$ vendor/bin/phpstan analyse ./array_example.php \
	--level=max -c phpstan.neon --no-progress --ansi
 ------ -----------------------------------------------------------------------------------------------
  Line   array_example.php
 ------ -----------------------------------------------------------------------------------------------
  4      Function array_func() has parameter $arr with no value type specified in iterable type array.
         💡 See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type
  4      Function array_func() return type has no value type specified in iterable type array.
         💡 See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type
 ------ -----------------------------------------------------------------------------------------------

 [ERROR] Found 2 errors

Array shapes fix

<?php

/**
    @param  array<string> $arr
    @return array<string>
*/
function array_func($arr) {
    return $arr;
}

Overview of static code analysis tools

PHPStan

  • Developed by Ondřej Mirtes

  • Install it (the simplest way is via Composer)

  • Configure it (optional)

  • Run it

lee@lee-VirtualBox:~/phpstan-example$ vendor/bin/phpstan analyse ./array_example.php
 1/1 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%



 [OK] No errors


💡 Tip of the Day:
PHPStan is performing only the most basic checks.
You can pass a higher rule level through the --level option
(the default and current level is 0) to analyse code more thoroughly.

lee@lee-VirtualBox:~/phpstan-example$

PHPStan Key Features

  • PHPStan will try to autoload unknown classes.

  • If some classes are not autoloaded, it will not be able to find them and will return an error.

  • If using magical methods via __call, __get, or __set, it can write a plug-in for PHPStan.

  • In actual fact, PHPStan doesn’t only perform autoload in the case of unknown classes, but it also does so for all classes.

  • Using neon-format for configuration.

  •  No support for its PHPDoc tags @phpstan-var, @phpstan-return etc.

  • PhpStan has a playground website https://phpstan.org.

Phan

  • Developed by the Etsy company. First commits by Rasmus Lerdorf.

  • Requiring the php-ast extension.

  • Plugin example is available here.

  •  Creating a .phan/config.php file.

  • Playground website is available.

lee@lee-VirtualBox:~/phpstan-example$ php vendor/bin/phan array_example.php
   analyze ████████████████████████████████████████████████████████████ 100.0% 29MB/29MB

lee@lee-VirtualBox:~/phpstan-example$ php vendor/bin/phan array_example.php
   analyze ████████████████████████████████████████████████████████████ 100.0% 28MB/31MB
array_example.php:9 PhanSyntaxError syntax error, unexpected '}', expecting ';' (at column 1)

Psalm

  • Developed by the Vimeo company

  • Annotations code

  • XML format file about configuration

  • Type aliases

    • ​array

    • closure

    • union type (for example, several classes or a class and other types)

    • enum

psalm.xml

<?xml version="1.0"?>
<psalm
    errorLevel="1"
    resolveFromConfigFile="true"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns="https://getpsalm.org/schema/config"
    xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd"
>
    <projectFiles>
        <directory name="src" />
        <ignoreFiles>
            <directory name="vendor" />
        </ignoreFiles>
    </projectFiles>
</psalm>

vendor/bin/psalm


░░░░░░░E░░░░E░E░░░EE░░░░░░░░░░░E░░░░E░░░░░E░E░░

ERROR: ParamNameMismatch - src/Element/Element.php:131:54 - Argument 2 of Innmind\Xml\Element\Element::replaceChild has wrong name $node, expecting $child as defined by Innmind\Xml\Node::replaceChild (see https://psalm.dev/230)
    public function replaceChild(int $position, Node $node): Node


ERROR: ParamNameMismatch - src/Element/SelfClosingElement.php:36:54 - Argument 2 of Innmind\Xml\Element\SelfClosingElement::replaceChild has wrong name $node, expecting $child as defined by Innmind\Xml\Node::replaceChild (see https://psalm.dev/230)
    public function replaceChild(int $position, Node $node): Node


ERROR: ParamNameMismatch - src/Node/CharacterData.php:43:54 - Argument 2 of Innmind\Xml\Node\CharacterData::replaceChild has wrong name $node, expecting $child as defined by Innmind\Xml\Node::replaceChild (see https://psalm.dev/230)
    public function replaceChild(int $position, Node $node): Node


ERROR: ParamNameMismatch - src/Node/Comment.php:43:54 - Argument 2 of Innmind\Xml\Node\Comment::replaceChild has wrong name $node, expecting $child as defined by Innmind\Xml\Node::replaceChild (see https://psalm.dev/230)
    public function replaceChild(int $position, Node $node): Node


ERROR: ParamNameMismatch - src/Node/Document.php:86:54 - Argument 2 of Innmind\Xml\Node\Document::replaceChild has wrong name $node, expecting $child as defined by Innmind\Xml\Node::replaceChild (see https://psalm.dev/230)
    public function replaceChild(int $position, Node $node): Node

CI/CD examples

GitHub Workflow examples

  1. Using Composer to install required development dependencies.

  2. GithubAction for PHP-CS-Fixer.
  3. PHP Static Analysis in Github Actions.

composer install

.......
psalm:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        php-version: ['7.4', '8.0']
    name: 'Psalm'
    steps:
      - name: Checkout
        uses: actions/checkout@v2
      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: ${{ matrix.php-version }}
          extensions: mbstring, intl
      - name: Get Composer Cache Directory
        id: composer-cache
        run: echo "::set-output name=dir::$(composer config cache-files-dir)"
      - name: Cache dependencies
        uses: actions/cache@v2
        with:
          path: ${{ steps.composer-cache.outputs.dir }}
          key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }}
          restore-keys: ${{ runner.os }}-composer-
      - name: Install Dependencies
        run: composer install
      - name: Psalm
        run: vendor/bin/psalm --shepherd
.......

GithubAction for PHP-CS-Fixer

PHP Static Analysis in Github Actions

Laravel framework integration

Psalm plugin for Laravel

nunomaduro/larastan

參考資料

Thanks!