Magento2 and Sonarqube

Set up sonarqube for magento2

Coding standards

While developing within Magento, there are a few coding standards that Magento suggests, a few of which are available for PHP, JS, HTML and LESS.

Besides Magento, we will also use the latest PSR-12 coding standard. To ensure guidelines, I advise the use of a editorconfig to ensure spaces, line endings and characterset are the same cross-platform. Most editors will have support, or a plugin to support, editorconfig files.

PHP MD

PHPMD stands for PHP Mess Detector and as its name suggests, it will analyze your code for cleanliness. An example:

phpmd app/code/MijnThuis/Custom text cleancode

/data/shared/sites/bpg-mijn-thuis/app/code/MijnThuis/Custom/Model/Config/Source/StorelocatorTag.php:56  The method toOptionArray uses an else expression. Else is never necessary and you can simplify the code to work without else.

Magento has some of their own rulesets for PHPMD which are used in integration tests:

find . -name ruleset.xml
./dev/tests/static/framework/Magento/ruleset.xml
./dev/tests/static/testsuite/Magento/Test/Less/_files/lesscs/ruleset.xml
./dev/tests/static/testsuite/Magento/Test/Php/_files/phpmd/ruleset.xml
./vendor/magento/magento2-base/dev/tests/static/framework/Magento/ruleset.xml
./vendor/magento/magento2-base/dev/tests/static/testsuite/Magento/Test/Less/_files/lesscs/ruleset.xml
./vendor/magento/magento2-base/dev/tests/static/testsuite/Magento/Test/Php/_files/phpmd/ruleset.xml

and some of the ruleset content is as follows:

<?xml version="1.0" encoding="UTF-8"?>
<!--
/**
 * Copyright © Magento, Inc. All rights reserved.
 * See COPYING.txt for license details.
 */
-->
<ruleset name="Magento">
    <description>LESS Coding Standard.</description>
    <rule ref="Generic.Files.EndFileNewline"/>
    <rule ref="Generic.Files.LineEndings"/>
    <rule ref="Generic.WhiteSpace.DisallowTabIndent"/>
    <rule ref="Squiz.CSS.NamedColours"/>
    <rule ref="../../../../../../framework/Magento/Sniffs/Less"/>
</ruleset>

The default available rulesets are: cleancode, codesize, controversial, design, naming, unusedcode but you can always create your own rulesets.

I noticed the phpmd output is not always entirely correct, as it sees private const as Unexpected token: const. As of PHP 7.1.0 visibility modifiers are allowed for class constants.

PHP CS

CS stands for Code Sniffer and it is a style checker. It can check for indentation, missing comments, naming convention, etc.

PHPUnit

PHPUnit is used for unittests in PHP and as generally accepted every piece of code should be properly tested. Although this is not in scope of this document, yet, I will not go deeper into testing.

Continuous Integration and Deployment

CI/CD is now done via Deployer and is integrated within the bitbucket pipelines.

Sonarqube

Sonarqube is a very powerful tool which checks both code quality and code security (by finding bugs and vulnerabilities based on a ruleset which is highly adjustable).

Quality checks with Sonarqube

Sonarqube is pretty easy to implement, all it takes to run it locally is a single command:

$ docker run -d --name sonarqube -p 9000:9000 sonarqube

After the sonarqube instance is up, you can download the sonar-scanner. After you downloaded the sonar-scanner, please create a new file called sonar-project.properties. Every project that runs through sonarqube should contain a sonar-project.properties file. This is where we define our sources which Sonarqube has to scan. This can be adjusted per project to our own needs, because there’s a difference between Magento2 modules and full projects.

Quality profiles

Sonarqube provides a default set of quality profiles and by default the sonar way is used. The sonar way has 107 active rules which check the code for vulnerabilities, bugs, code smells and security hotspots. There are also specific PHP / PSR-2 quality profiles available which help with adhering to coding standards. If needed we could create a profile based on Magento2, but for now we could go a long way with the default profile(s).

Quality gates

Quality gates are a set of conditions which the project should meet. Default conditions are

| Metric                             | Operator        | Error |
|------------------------------------|-----------------|-------|
| Coverage on New Code               | Is less than    | 80%   |
| Duplicated Lines on New Code       | Is greater than | 3.0%  |
| Maintainability Rating on New Code | Is worse than   | A     |
| Reliability Rating on New Code     | Is worse than   | A     |
| Security Rating on New Code        | Is worse than   | A     |

If one of these conditions is not met, the quality gate will fail and so should the bitbucket pipelines build. Afterwards it is up to the developer to figure out why the build failed and fix the changes. The quality gate check should be done before deploying to any environment to ensure the quality of the code. We have the option to define multiple quality gates.

Qualitygate in pipelines

Sonarqube provides an API which you can use to fetch the actual qualitygate status per project.

# -k skips cert validation
# -G is --get or -X GET and makes sure -d params are parsed as GET parameters
curl -k -G https://sonarqube.mycha.o.xotap.nl/api/qualitygates/project_status \
-d projectKey=mertens-groep

and this will show you per condition if it matches or not, with the last status being ERROR or OK. These are the values that can be used in quality gate passing or failing during CI/CD. More on the api can be found here

Sonarqube should be integrated in the pipelines and the quality gate check should be either passing or failing, which can be checked using the sonarqube API as mentioned above.

Sonar properties sample file

To let your project be scanned by sonarqube (and trigger a manual scan), you should add a sonar-project.properties file in the root of your project. An example for Magento2:

sonar.projectKey=test-project
sonar.projectName=Test Project
sonar.sources=app/code
sonar.exclusions=**/Test/**, **/registration.php

sonar.test.exclusions=**/Test/**
sonar.php.coverage.reportPaths=reports/clover.xml

sonar.login=mycha
sonar.password=admin

The sample file above will only include code in the app/code directory and will exclude test files and registration.php files.

Sonarqube and test coverage

Sonarqube will automatically pick up code test coverage percentages. In order to set Magento up to include tests from app/code (and in my case also from work, which is vendor/xcom), you could add the following file in dev/tests/unit/phpunit.xml

<?xml version="1.0" encoding="UTF-8"?>
<!--
/**
 * Copyright © Magento, Inc. All rights reserved.
 * See COPYING.txt for license details.
 * @category   Xcom PHPUnit
 * @copyright  Copyright (c) 2019 X-Com B.V. (http://www.x-com.nl)
 * @author     Mycha de Vrees
 * usage
 * php ./vendor/bin/phpunit -c dev/tests/unit/phpunit.xml (double-hyphen)testsuite Magento_Vendor_Unit_Tests_App_Code
 */
-->
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/6.2/phpunit.xsd"
         colors="true"
         columns="max"
         beStrictAboutTestsThatDoNotTestAnything="false"
         bootstrap="./framework/bootstrap.php"
        >
    <testsuite name="Magento_Vendor_Unit_Tests_App_Code">
        <directory suffix="Test.php">../../../vendor/xcom/*/Test/Unit</directory>
        <directory suffix="Test.php">../../../app/code/*/*/Test/Unit</directory>
        <exclude>../../../app/code/*/*/registration.php</exclude>
        <exclude>../../../vendor/xcom/*/registration.php</exclude>
    </testsuite>
    <testsuite name="Magento_Unit_Tests_Other">
        <directory suffix="Test.php">../../../lib/internal/*/*/Test/Unit</directory>
        <directory suffix="Test.php">../../../lib/internal/*/*/*/Test/Unit</directory>
        <directory suffix="Test.php">../../../setup/src/*/*/Test/Unit</directory>
        <directory suffix="Test.php">../../../vendor/*/module-*/Test/Unit</directory>
        <directory suffix="Test.php">../../../vendor/*/framework/Test/Unit</directory>
        <directory suffix="Test.php">../../../vendor/*/framework/*/Test/Unit</directory>
        <directory suffix="Test.php">../../tests/unit/*/Test/Unit</directory>
    </testsuite>
    <php>
        <ini name="date.timezone" value="Europe/Amsterdam"/>
        <ini name="xdebug.max_nesting_level" value="200"/>
    </php>
    <filter>
       <whitelist addUncoveredFilesFromWhiteList="true">
            <directory suffix=".php">../../../app/code/*</directory>
            <directory suffix=".php">../../../lib/internal/Magento</directory>
            <directory suffix=".php">../../../setup/src/*</directory>
            <directory suffix=".php">../../../vendor/xcom/*</directory>
            <exclude>
                <directory>../../../app/code/*/*/Test</directory>
                <directory>../../../lib/internal/*/*/Test</directory>
                <directory>../../../lib/internal/*/*/*/Test</directory>
                <directory>../../../setup/src/*/*/Test</directory>
                <directory suffix="Test.php">../../../app/code/*/*/Test/Unit</directory>
                <directory suffix="Test.php">../../../vendor/xcom/*/Test/Unit</directory>
            </exclude>
        </whitelist>
    </filter>
    <listeners>
        <listener class="Yandex\Allure\Adapter\AllureAdapter">
            <arguments>
                <string>var/allure-results</string> <!-- XML files output directory -->
                <boolean>true</boolean> <!-- Whether to delete previous results on rerun -->
                <array> <!-- A list of custom annotations to ignore (optional) -->
                    <element key="codingStandardsIgnoreStart">
                        <string>codingStandardsIgnoreStart</string>
                    </element>
                    <element key="codingStandardsIgnoreEnd">
                        <string>codingStandardsIgnoreEnd</string>
                    </element>
                    <element key="cover">
                        <string>cover</string>
                    </element>
                    <element key="expectedExceptionMessageRegExp">
                        <string>expectedExceptionMessageRegExp</string>
                    </element>
                </array>
            </arguments>
        </listener>
        <listener class="Magento\Framework\TestFramework\Unit\Listener\ReplaceObjectManager"/>
    </listeners>
    <logging>
        <!--coverage_html_placeholder -->
            <log type="coverage-html" target="test-reports/coverage" charset="UTF-8" yui="true" highlight="true"/>
        <!-- coverage_html_placeholder-->
        <!--coverage_cov_placeholder
            <log type="coverage-php" target="{{coverage_dir}}/test-reports/coverage.cov"/>
        coverage_cov_placeholder-->
        <!--coverage_clover_placeholder -->
            <log type="coverage-clover" target="test-reports/phpunit.coverage.xml"/>
        <!--coverage_clover_placeholder-->
        <!--coverage_crap4j_placeholder
            <log type="coverage-crap4j" target="{{coverage_dir}}/test-reports/phpunit.crap4j.xml"/>
        coverage_crap4j_placeholder-->
    </logging>
</phpunit>

See also