The Magento Coding Standard

Install the Magento2 phpcs ruleset, run it on one module, and read the severities that mark real bugs rather than style.

Syntaxvendor/bin/phpcs --standard=Magento2 app/code/Vendor/Loyalty

The Magento Coding Standard is a set of rules for PHP_CodeSniffer, shipped as the Composer package magento/magento-coding-standard. The ruleset inside it is named Magento2. It is more than a formatter. Its highest-severity rules find unescaped output in templates, raw SQL, superglobals, and the install and upgrade scripts that Magento 2.4.9 still executes but that declarative schema and patches have replaced.

Installing and running it

Add it with composer require --dev magento/magento-coding-standard. Composer cannot register the ruleset with PHP_CodeSniffer for you, so run vendor/bin/phpcs --config-set installed_paths ../../magento/magento-coding-standard/ once, then confirm with vendor/bin/phpcs -i that Magento2 appears in the list. Check a module with vendor/bin/phpcs --standard=Magento2 app/code/Vendor/Loyalty. The ruleset declares the file extensions it reads itself: php, phtml, graphqls, less, html, xml and js. Your layout XML and your templates are checked, not only your classes.

Reading the severities

Every rule in the ruleset carries a severity. The ones that matter are set to 10 and reported as errors.

Sniff codeSeverityReported as
Magento2.Security.XssTemplate.FoundUnescaped10error
Magento2.Legacy.InstallUpgrade10error
Magento2.SQL.RawQuery9warning
Magento2.Templates.ObjectManager8warning
Magento2.Annotation5warning

What trips people up

phpcs reads files from disk and needs nothing from Magento, so no command applies its findings. vendor/bin/phpcbf --standard=Magento2 does rewrite files in place. After it touches a class or a template, run bin/magento cache:flush before judging the storefront.

Example

Example · xml
<!-- phpcs.xml (project root, next to composer.json) -->
<ruleset name="VendorLoyalty">
    <description>The Magento2 standard, scoped to one module.</description>

    <!-- Shorthand. "sp" is the same as passing -s -p on the command line:
         -s shows the sniff code next to each message, -p shows progress. -->
    <arg value="sp"/>
    <arg name="colors"/>

    <!-- Default path, so plain `vendor/bin/phpcs` checks the module. -->
    <file>app/code/Vendor/Loyalty</file>

    <exclude-pattern>*/generated/*</exclude-pattern>
    <exclude-pattern>*/Test/_files/*</exclude-pattern>

    <!-- The whole ruleset. Identical to --standard=Magento2 on the CLI. -->
    <rule ref="Magento2"/>

    <!-- Keep the security and schema rules loud, quieten the doc-block rules
         while the module is being brought up to standard. -->
    <rule ref="Magento2.Annotation">
        <severity>1</severity>
    </rule>
</ruleset>

When to use it

  • Gate a module's pull requests on the Magento2 ruleset before submitting it to the Marketplace.
  • Find every unescaped variable in a theme's templates after taking over a legacy project.
  • Confirm a module has no InstallSchema or UpgradeData scripts left before a 2.4.9 upgrade.
  • Normalise a module's spacing and import order with phpcbf instead of by hand.

More examples

Registering the standard from composer.json

The installed_paths setting lives in PHP_CodeSniffer's own config file, which Composer will not write for security reasons. These two scripts set it after every install and update, and the $COMPOSER_DEV_MODE guard keeps them from running on a production build where the package is absent. Reach for this on any project where more than one person runs phpcs, so nobody has to remember the config-set step.

Example · json
// composer.json (fragment, project root)
{
    "require-dev": {
        "magento/magento-coding-standard": "*"
    },
    "scripts": {
        "post-install-cmd": [
            "([ $COMPOSER_DEV_MODE -eq 0 ] || vendor/bin/phpcs --config-set installed_paths ../../magento/magento-coding-standard/)"
        ],
        "post-update-cmd": [
            "([ $COMPOSER_DEV_MODE -eq 0 ] || vendor/bin/phpcs --config-set installed_paths ../../magento/magento-coding-standard/)"
        ]
    }
}

What a run looks like, and fixing what can be fixed

The -s flag prints the sniff code, which is the part you need to look a rule up or to exclude it in phpcs.xml. Note the split: the unescaped template is an error and blocks a build, the raw SQL is a warning because the sniff cannot tell a bound query from an interpolated one. phpcbf fixes whitespace and import order; it will not fix either of these two, because both need a decision about the code.

Example · bash
$ vendor/bin/phpcs --standard=Magento2 -s app/code/Vendor/Loyalty

FILE: app/code/Vendor/Loyalty/view/frontend/templates/balance.phtml
--------------------------------------------------------------------
FOUND 1 ERROR AFFECTING 1 LINE
--------------------------------------------------------------------
 14 | ERROR | Unescaped output detected.
    |       | (Magento2.Security.XssTemplate.FoundUnescaped)
--------------------------------------------------------------------

FILE: app/code/Vendor/Loyalty/Model/ResourceModel/Balance.php
--------------------------------------------------------------------
FOUND 0 ERRORS AND 1 WARNING AFFECTING 1 LINE
--------------------------------------------------------------------
 61 | WARNING | Possible raw SQL statement SELECT detected.
    |         | (Magento2.SQL.RawQuery.FoundRawSql)
--------------------------------------------------------------------

# Fix only the mechanical violations (spacing, imports, line endings):
$ vendor/bin/phpcbf --standard=Magento2 app/code/Vendor/Loyalty

The violation the XSS sniff is looking for

The sniff reads the printed expression and reports anything that is not wrapped in an escaper call, which is why it runs on .phtml files only. The short echo tag <?= and <?php echo are identical to PHP and identical to the sniff; neither is safer than the other. Reach for this when you inherit a theme and need a list of every template that prints a raw variable. After editing templates, run bin/magento cache:flush so the storefront stops serving the cached block output.

Example · php
<?php // app/code/Vendor/Loyalty/view/frontend/templates/balance.phtml ?>
<?php /** @var \Magento\Framework\Escaper $escaper */ ?>
<?php /** @var \Vendor\Loyalty\ViewModel\Balance $viewModel */ ?>

<?php // Flagged: Magento2.Security.XssTemplate.FoundUnescaped ?>
<p><?= $viewModel->getTierName() ?></p>

<?php // Accepted. <?= is shorthand for <?php echo; they are the same thing. ?>
<p><?= $escaper->escapeHtml($viewModel->getTierName()) ?></p>
<p><?php echo $escaper->escapeHtml($viewModel->getTierName()); ?></p>

<?php // The method has to match the context, not the data. ?>
<a href="<?= $escaper->escapeUrl($viewModel->getRewardsUrl()) ?>"
   title="<?= $escaper->escapeHtmlAttr($viewModel->getTierName()) ?>">
    <?= $escaper->escapeHtml(__('Your rewards')) ?>
</a>

Discussion

  • Be the first to comment on this lesson.