Find the ultimate configurations to automate code quality checks using GrumpPHP in Magento 2

TLDR; this article is tutorial setting up GrumPHP task runner for quality checks in Magento 2. An earlier video tutorial and presentation by me is already there. Check out: https://youtu.be/tq-DPi9wMss and the presentation https://slides.com/milindsingh/virtual-magento-meetup/

  1. why automate?

    1. developers are (a little) lazy

      • running 6-7 tools manually is what I will try to skip few times ( and expect it would pass in deployment pipelines )
      • we are overburdened (most of the times)
    2. development pipelines will fail

      • suppose we miss a space at the end of the file, and phpcs fails while running the automated pipelines on pull request merge.
      • fix the change and again push just to add a simple line at the end of file (time taking)
    3. automation can be enforced

      • grumphp can be configured to listen to git commit commands and will not allow until all quality checks passed.
  2. what to automate?

    1. PHP Code Sniffer

    2. PHP Code Beautifier

      • automatically correct coding standard violations
    3. PHP Coding Standards Fixer

    4. PHP Mess Detector

    5. PHP Stan

    6. Keywords

      • print_r("test")
      • die("hi")
      • ObjectManager::getInstance()
    7. Custom

  3. how to automate?

    • GrumPHP

      GrumPHP has a set of common tasks built-in.

      GrumPHP will run some tests on the committed code. If the tests fail, you won't be able to commit your changes.

      This handy tool will not only improve your codebase, it will also teach your co-workers to write better code following the best practices you've determined as a team.

    • Installation

      Install GrumPHP by running

      • composer require --dev phpro/grumphp (in current project only) or,
      • composer global require --dev phpro/grumphp (globally recommended)
    • Dependencies

      • PHPCS:

        • Install PHP CodeSniffer (skip if already installed) :

          • composer global require --dev "squizlabs/php_codesniffer=*" (globally recommended)
          • composer require --dev "squizlabs/php_codesniffer=*" (at project level, need to add project-root/vendor/bin to PATH for direct cli use)
        • Goto Magento2 root run following commands to install Magento2 coding standard :

          composer require --dev magento/magento-coding-standard

        • Set Magento2 Standard in PHP CodeSniffer available standards:

          phpcs --config-set installed_paths ../../magento/magento-coding-standard/

      • PHPCS Fixer 2

        Install PHP Coding Standards Fixer (skip if already installed) :

        • composer global require --dev friendsofphp/php-cs-fixer (globally recommended)
        • composer require --dev friendsofphp/php-cs-fixer (at project level, need to add project-root/vendor/bin to PATH for direct cli use)
      • PHPStan

        • composer global require --dev phpstan/phpstan (globally recommended)
        • composer require --dev phpstan/phpstan (at project level, need to add project-root/vendor/bin to PATH for direct cli use)
      • PHPMD

        • composer global require --dev phpmd/phpmd (globally recommended)
        • composer require --dev phpmd/phpmd (at project level, need to add project-root/vendor/bin to PATH for direct cli use)
    • Setup

      GrumPHP can monitor each git repository push action by initializing it in the repository. GrumPHP can be configured at 2 levels:

      • Project Level Setup

        • Goto the magento-2-root and run:

          • php vendor/bin/grumphp git:init or grumphp git:init (recommended)
        • Create a grumphp.yml file in magento-2-root and copy all content as below code.
        • Though GrumPHP auto detect git commit command but you can manually test by running php vendor/bin/grumphp run or grumphp run inside in magento-2-root.

        external:code:github

        # Project level GrumPHP configuration for Magento 2
        grumphp:
          hide_circumvention_tip: true
          process_timeout: 120
          stop_on_failure: false
          ignore_unstaged_changes: false
          tasks:
            jsonlint:
              detect_key_conflicts: true
              metadata:
                priority: 100
            xmllint:
              ignore_patterns:
                - "#test/(.*).xml#"
              metadata:
                priority: 100
            phplint:
              triggered_by: ['php', 'phtml']
              metadata:
                priority: 200
            yamllint:
              ignore_patterns:
                - "#test/(.*).yml#"
                - "#charts/.*#"
              metadata:
                priority: 100
            composer:
              file: ./composer.json
              no_check_all: true
              no_check_lock: false
              no_check_publish: false
              with_dependencies: false
              strict: false
              metadata:
                priority: 80
            # validate git commit message
            git_commit_message:
              allow_empty_message: false
              enforce_capitalized_subject: false
              enforce_no_subject_punctuations: false
              enforce_no_subject_trailing_period: true
              enforce_single_lined_subject: true
              type_scope_conventions: []
              max_body_width: 80
              max_subject_width: 80
              matchers:
                "Commit message must contain issue topic and number": /^\[(HOTFIX|BUGFIX|FEATURE|INFRA|MERGE|RELEASE)]\sICRSICRP-\d+\s::\s.*\s\[(COMPLETED|WIP)]/
              case_insensitive: true
              multiline: false
              additional_modifiers: ''
            # validate git branch names
            git_branch_name:
              whitelist:
                - "/(hotfix|bugfix|feature|release)\\/ICRSICRP\\-([0-9]+)$/"
              blacklist:
                - "development"
                - "production"
                - "staging"
                - "master"
                - "infra"
              allow_detached_head: true
            # catch not allowed keywords
            git_blacklist:
              keywords:
                - "\\.dev"
                - "\\.local"
                - "\\.test"
                - "<<<<<<<"
                - "======="
                - "DebuggerUtility"
                - "ObjectManager::getInstance"
                - "_GET\\["
                - "_POST\\["
                - "_REQUEST\\["
                - "console.log("
                - "die("
                - "die;"
                - "exit("
                - "exit;"
                - "fileadmin"
                - "localhost"
                - "phpinfo"
                - "phpinfo("
                - "print_r("
                - "var_dump("
                - "_objectManager"
                - "ObjectManagerInterface"
              triggered_by: ['php', 'js', 'html', 'phtml']
              metadata:
                priority: 90
            # https://devdocs.magento.com/guides/v2.4/coding-standards/code-standard-php.html
            phpcs:
              standard: Magento2
              tab_width: 4
              severity: 10 # can remove this to dis allow all level of severity.
              error_severity: 10
              warning_severity: ~
              report: full
              triggered_by: [phtml, php]
              metadata:
                priority: 70
            phpcsfixer2:
              allow_risky: false
              config: '.php_cs.dist'
              triggered_by: ['php', 'phtml']
              using_cache: true
              cache_file: './.php_cs.cache'
              config_contains_finder: true
              verbose: true
            phpmd:
              ruleset: ['./dev/tests/static/testsuite/Magento/Test/Php/_files/phpmd/ruleset.xml']
              triggered_by: ['php']
              exclude:
                - "./app/code/Magento/"
                - "./app/code/*/*/Setup/"
              metadata:
                priority: 70
            #  uncomment to skip modules using whitelist patterns
            #  whitelist_patterns:
            #     - /^app\/code\/MyVendor\/MyModuleToSkip\/(.*)/
        
            # https://devdocs.magento.com/guides/v2.4/test/testing.html#phpstan
            phpstan:
              autoload_file: ~
              configuration: './dev/tests/static/testsuite/Magento/Test/Php/_files/phpstan/phpstan.neon'
              level: 8
              triggered_by: ['php']
              force_patterns: []
              ignore_patterns: []
              memory_limit: "-1"
              metadata:
                    priority: 90
            phpversion:
              project: '7.3'
      • Module Level Setup

        • Goto the module directory i.e. magento-2-root/app/code/MyVendor/MyModule and run:

          • php vendor/bin/grumphp git:init or grumphp git:init (recommended)
        • Create a grumphp.yml file in magento-2-root/app/code/MyVendor/MyModule and copy all content as below code.
        • Add bin path magento2-root/vendor/bin to your module composer.json. Refer to composer.json.sample.

          {
              "config": {
                      "bin-dir": "../../../../vendor/bin"
                  }
          }
        • Same as project level setup, GrumPHP auto detect git commit command but you can manually test by running php ../../../../vendor/bin/grumphp run or grumphp run inside module.

        external:code:github

        # Module level GrumPHP configuration for Magento 2
        grumphp:
          hide_circumvention_tip: true
          process_timeout: 120
          stop_on_failure: false
          ignore_unstaged_changes: false
          tasks:
            jsonlint:
              detect_key_conflicts: true
              metadata:
                priority: 100
            xmllint:
              ignore_patterns:
                - "#test/(.*).xml#"
              metadata:
                priority: 100
            phplint:
              triggered_by: ['php', 'phtml']
              metadata:
                priority: 200
            yamllint:
              ignore_patterns:
                - "#test/(.*).yml#"
                - "#charts/.*#"
              metadata:
                priority: 100
            composer:
              file: ./composer.json
              no_check_all: true
              no_check_lock: false
              no_check_publish: false
              with_dependencies: false
              strict: false
              metadata:
                priority: 80
            # validate git commit message
            git_commit_message:
              allow_empty_message: false
              enforce_capitalized_subject: false
              enforce_no_subject_punctuations: false
              enforce_no_subject_trailing_period: true
              enforce_single_lined_subject: true
              type_scope_conventions: []
              max_body_width: 80
              max_subject_width: 80
              matchers:
                "Commit message must contain issue topic and number": /^\[(HOTFIX|BUGFIX|FEATURE|INFRA|MERGE|RELEASE)]\sICRSICRP-\d+\s::\s.*\s\[(COMPLETED|WIP)]/
              case_insensitive: true
              multiline: false
              additional_modifiers: ''
            # validate git branch names
            git_branch_name:
              whitelist:
                - "/(hotfix|bugfix|feature|release)\\/ICRSICRP\\-([0-9]+)$/"
              blacklist:
                - "development"
                - "production"
                - "staging"
                - "master"
                - "infra"
              allow_detached_head: true
            # catch not allowed keywords
            git_blacklist:
              keywords:
                - "\\.dev"
                - "\\.local"
                - "\\.test"
                - "<<<<<<<"
                - "======="
                - "DebuggerUtility"
                - "ObjectManager::getInstance"
                - "_GET\\["
                - "_POST\\["
                - "_REQUEST\\["
                - "console.log("
                - "die("
                - "die;"
                - "exit("
                - "exit;"
                - "fileadmin"
                - "localhost"
                - "phpinfo"
                - "phpinfo("
                - "print_r("
                - "var_dump("
                - "_objectManager"
                - "ObjectManagerInterface"
              triggered_by: ['php', 'js', 'html', 'phtml']
              metadata:
                priority: 90
            # https://devdocs.magento.com/guides/v2.4/coding-standards/code-standard-php.html
            phpcs:
              standard: Magento2
              tab_width: 4
              severity: 10 # can remove this to dis allow all level of severity.
              error_severity: 10
              warning_severity: ~
              report: full
              triggered_by: [phtml, php]
              metadata:
                priority: 70
            phpcsfixer2:
              allow_risky: false
              config: '../../../../.php_cs.dist'
              triggered_by: ['php', 'phtml']
              using_cache: true
              cache_file: './.php_cs.cache'
              config_contains_finder: true
              verbose: true
            phpmd:
              ruleset: ['../../../../dev/tests/static/testsuite/Magento/Test/Php/_files/phpmd/ruleset.xml']
              triggered_by: ['php']
              exclude:
                - "./app/code/Magento/"
                - "./app/code/*/*/Setup/"
              metadata:
                priority: 70
            #  uncomment to skip modules using whitelist patterns
            #  whitelist_patterns:
            #     - /^app\/code\/MyVendor\/MyModuleToSkip\/(.*)/
        
            # https://devdocs.magento.com/guides/v2.4/test/testing.html#phpstan
            phpstan:
              autoload_file: ~
              configuration: '../../../../dev/tests/static/testsuite/Magento/Test/Php/_files/phpstan/phpstan.neon'
              level: 8
              triggered_by: ['php']
              force_patterns: []
              ignore_patterns: []
              memory_limit: "-1"
              metadata:
                    priority: 90
            phpversion:
              project: '7.3'