Coding Standards and Static Analysis for Joomla Extensions

This content was generated with the assistance of AI and should be verified against the official Joomla documentation before being relied on in production.

This sub-section’s Joomla bibliography lists the reference material consulted while preparing these pages.

This page shows how to hold a Joomla extension to the same style and static-analysis bar as the core. It was written against Joomla 6.2.x and PHP 8.3+ and checked on 2026-10-30 against the Joomla Developer Manual (Coding Style Guide) and the 6.2-dev branch of joomla-cms, whose ruleset.xml, .php-cs-fixer.dist.php, phpstan.neon, build/eslint.config.mjs, build/.stylelintrc.json, .editorconfig, composer.json and package.json are the reference configuration used below. The running example is the pkg_tallpine package of the Tallpine Hiking Club site: the component com_tallpine (namespace Tallpine\Component\Tallpine), the module mod_tallpine_nexthikes and the plugins plg_content_tallpine_conditions and plg_task_tallpine_trailsync.

The audience is an extension developer or maintainer. General PHP practice is not re-taught here: PSR-1, PSR-4 and PSR-12 are described in PSR standards and coding style, and PHPStan levels, baselines and Rector in Static analysis and refactoring. What follows is the Joomla-specific part: which standard applies, how the core configures each tool, and how to transplant that configuration into an extension repository. Wiring the tools into GitHub Actions and the release build is covered in Build and CI for extensions, and unit and system tests in Testing extensions.

Why a standard, and who uses it

A coding standard removes style from code review, keeps diffs small and lets tools fail a build on mechanical problems. In Joomla the audience is wider than the core team:

  • extension developers who want their code to look and behave like core code, and who copy core classes as starting points

  • contributors sending pull requests to joomla-cms, whose CI rejects code-style violations

  • maintainers of long-lived extensions who need to catch removed or deprecated APIs before a major release (see Deprecations and preparing for Joomla 7)

Static analysis goes one step further than style. It reads the code without running it and reports calls to missing methods, wrong argument types, undefined variables and, with the deprecation rules, uses of APIs that Joomla has marked for removal. Neither a style checker nor a static analyser is a security scanner, but both remove whole classes of defects that otherwise reach production (see Security implications).

The Joomla coding standard

PSR-12 since Joomla 4.2

The Joomla Manual states that since Joomla 4.2 the project uses the PSR-12 coding standard, and recommends that extension developers follow it as well so that their code stays consistent with core. Earlier releases used a Joomla-specific standard, which is why older tutorials, IDE presets and snippets show different brace placement and indentation. PSR-12 (Extended Coding Style) builds on PSR-1 (basic standard) and PSR-4 (autoloading); the essentials that matter for Joomla code are:

  • four spaces for indentation in PHP, no tabs, Unix line endings and one blank line at the end of the file

  • opening brace of a class or method on its own line, opening brace of a control structure on the same line

  • one use statement per imported class, grouped and ordered, one blank line after the import block

  • StudlyCaps class names, camelCase method names, UPPER_CASE class constants

  • visibility declared on every property, method and constant; the abstract and final keywords before visibility, static after it

The Manual page’s installation section points to the legacy wiki page Joomla CodeSniffer for older instructions, so the commands below come from the core repository itself, which is the authoritative description of how the standard is enforced.

What the Joomla ruleset adds and relaxes

Joomla does not use the stock PSR12 standard unchanged. The ruleset.xml at the root of joomla-cms is named Joomla-CMS, described as "the Joomla CMS PSR-12 exceptions", and does the following:

  • Starts from PSR12 with a single rule ref line.

  • Excludes folders without production code: administrator/cache, administrator/logs, build, cache, media, node_modules, tmp, and the compatibility classes of the Behaviour - Backward Compatibility 6 plugin.

  • Excludes third-party and framework code: libraries/vendor, libraries/php-encryption, libraries/phpass, and the generated configuration.php.

  • Raises the line-length limit of Generic.Files.LineLength to 560 characters. The ruleset marks this as a temporary extension, so do not copy it blindly; a limit of 120 characters (the PSR-12 soft limit) is the sensible default for a new extension.

  • Exempts a list of legacy files from PSR2.Classes.PropertyDeclaration.Underscore. Older table and model classes keep protected properties with a leading underscore for backward compatibility. New code must not add them.

  • Exempts the entry-point scripts (index.php, includes/defines.php, includes/app.php, cli/joomla.php, the installation scripts and the libraries loader files) from the PSR-1 rule that a file declares symbols or causes side effects, but not both. That is a bootstrap concern; an extension class never needs the exemption.

The most visible consequence for extension code is the access guard. The PSR-1 side-effects sniff of PHP_CodeSniffer has a special case for defined(), so the guard line defined('_JEXEC') or die; placed after the namespace statement does not trigger a violation even in a file that declares a class. Joomla 6.0 no longer defines JPATH_PLATFORM unless the Behaviour - Backward Compatibility 6 plugin is enabled, so guards that test it must be changed to _JEXEC; see Upgrading Joomla 5 to 6.

File header and layout files

Core PHP files start with a docblock giving @package, @subpackage, @copyright and @license. Extensions normally copy the shape and substitute their own package name, copyright holder and licence. Joomla extensions distributed through the Joomla Extensions Directory must be GPL-compatible. The header precedes the namespace statement and is separated from it by a blank line:

<?php

/**
 * @package     Tallpine.Administrator
 * @subpackage  com_tallpine
 *
 * @copyright   (C) 2026 Tallpine Hiking Club
 * @license     GNU General Public License version 2 or later; see LICENSE.txt
 */

namespace Tallpine\Component\Tallpine\Administrator\Model;

use Joomla\CMS\MVC\Model\ListModel;

class TrailsModel extends ListModel
{
    /**
     * Fields that may be used for filtering and ordering the trail list.
     *
     * @var    string[]
     * @since  1.0.0
     */
    protected $filter_fields = ['id', 'title', 'distance_km', 'difficulty', 'published'];
}

Layout (tmpl) files are a special case. They mix HTML and PHP and use the alternative control syntax (foreach (...): and endforeach;), so a formatter tuned for classes rewrites them badly. The core .php-cs-fixer.dist.php therefore switches off a list of rules (binary_operator_spaces, native_function_invocation, statement_indentation, single_space_around_construct, braces_position, trailing_comma_in_multiline and others) for any file whose path contains a tmpl or layouts folder, or the atum, cassiopeia or cassiopeia_extended template folders. The same carve-out belongs in an extension that ships layouts; see PHP-CS-Fixer for an extension. Layouts are described in Layouts and module chromes.

EditorConfig

The core ships an .editorconfig that tools and editors read before any linter does. Reusing it keeps the editor, PHPCS, PHP-CS-Fixer and the JavaScript linters in agreement about indentation and line endings:

# .editorconfig of pkg_tallpine (derived from the joomla-cms root file)
root = true

[*]
indent_style = tab
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

[*.php]
indent_style = space
indent_size = 4

[*.{mjs,js,json,scss,css,yml,vue}]
indent_style = space
indent_size = 2

Layout of an extension repository

The examples below assume the following repository for the Tallpine package. Keep the tooling configuration at the repository root and the Joomla-installable sources in subfolders; the build step (see Build and CI for extensions) then zips only the subfolders.

tallpine/
|-- composer.json            dev tools, autoload-dev for the Tallpine namespaces
|-- package.json             ESLint, Stylelint, build scripts
|-- phpcs.xml                PHP_CodeSniffer ruleset
|-- .php-cs-fixer.dist.php   PHP-CS-Fixer configuration
|-- phpstan.neon             PHPStan configuration
|-- phpstan-baseline.neon    accepted existing errors (generated)
|-- eslint.config.mjs        ESLint flat configuration
|-- .stylelintrc.json        Stylelint configuration
|-- .editorconfig
|-- com_tallpine/
|   |-- admin/src/           namespace Tallpine\Component\Tallpine\Administrator
|   |-- admin/tmpl/
|   |-- site/src/            namespace Tallpine\Component\Tallpine\Site
|   `-- media_source/        SCSS and JavaScript sources, compiled into media/
|-- mod_tallpine_nexthikes/
`-- plugins/

The extension namespaces are declared in each manifest with a namespace element, which is how Joomla autoloads the classes at run time (see Namespaces, autoloading and dependency injection). Analysis tools do not read manifests, so the repository composer.json must repeat the mapping in autoload-dev:

{
  "name": "tallpine/pkg-tallpine",
  "description": "Tallpine Hiking Club package for Joomla 6",
  "type": "project",
  "license": "GPL-2.0-or-later",
  "require": {
    "php": ">=8.3"
  },
  "require-dev": {
    "squizlabs/php_codesniffer": "^3.13",
    "friendsofphp/php-cs-fixer": "^3.94",
    "phpstan/phpstan": "^2.1",
    "phpstan/phpstan-deprecation-rules": "^2.0",
    "phpunit/phpunit": "^12.5"
  },
  "autoload-dev": {
    "psr-4": {
      "Tallpine\\Component\\Tallpine\\Administrator\\": "com_tallpine/admin/src/",
      "Tallpine\\Component\\Tallpine\\Site\\": "com_tallpine/site/src/",
      "Tallpine\\Module\\NextHikes\\Site\\": "mod_tallpine_nexthikes/src/",
      "Tallpine\\Plugin\\Content\\TallpineConditions\\": "plugins/content/tallpine_conditions/src/"
    }
  },
  "config": {
    "sort-packages": true,
    "audit": { "abandoned": "report" }
  },
  "scripts": {
    "cs": "phpcs --standard=phpcs.xml",
    "cs:fix": "phpcbf --standard=phpcs.xml",
    "fixer": "php-cs-fixer fix -vvv --dry-run --diff",
    "stan": "phpstan analyse --memory-limit=1G",
    "check": ["@cs", "@fixer", "@stan"]
  }
}

The version ranges mirror what the core pins in require-dev on 6.2-dev (PHP_CodeSniffer 3.13.6 or later, PHP-CS-Fixer 3.94.2 or later, PHPStan 2.1.40 or later with phpstan-deprecation-rules 2.0.4 or later, PHPUnit 12.5 or later, platform PHP 8.3.0). Dev dependencies belong to the repository, not to the installable zip.

PHP_CodeSniffer and PHPCBF

Running the core tools

The Manual explains that the full developer checkout of Joomla (from GitHub) contains the code sniffer and code fixer, while the installation zip does not, because it carries no dev dependencies. From the Joomla root folder the checker and fixer are run as follows; the trailing . is part of the command and is required:

# shortcut defined in the composer.json of joomla-cms
composer run phpcs

# the underlying command: check the whole installation, including installed extensions
./libraries/vendor/bin/phpcs --extensions=php -p --standard=ruleset.xml .

# fix what can be fixed automatically, then re-run the checker for the rest
./libraries/vendor/bin/phpcbf --extensions=php -p --standard=ruleset.xml .

Because Joomla’s Composer vendor-dir is libraries/vendor, the binaries live in libraries/vendor/bin, not in the usual vendor/bin. That is a frequent source of "command not found" when following generic PHP tutorials in a Joomla checkout. In your own extension repository the default vendor/bin applies.

A ruleset for the Tallpine extensions

phpcs (reports) and phpcbf (fixes) share one ruleset. The following phpcs.xml applies PSR-12 to the Tallpine sources only, ignores generated and third-party content, and keeps the PSR-12 line limit. It is a derivative of the core ruleset, not a copy:

<?xml version="1.0"?>
<ruleset name="Tallpine">
    <description>PSR-12 for the Tallpine Hiking Club extensions.</description>

    <file>com_tallpine</file>
    <file>mod_tallpine_nexthikes</file>
    <file>plugins</file>

    <arg name="extensions" value="php"/>
    <arg name="colors"/>
    <arg value="ps"/>

    <exclude-pattern>*/vendor/*</exclude-pattern>
    <exclude-pattern>*/node_modules/*</exclude-pattern>
    <exclude-pattern>*/media/*</exclude-pattern>
    <exclude-pattern>*/language/*</exclude-pattern>

    <rule ref="PSR12"/>

    <rule ref="Generic.Files.LineLength">
        <properties>
            <property name="lineLimit" value="120"/>
            <property name="absoluteLineLimit" value="160"/>
        </properties>
    </rule>

    <!-- Layout files: alternative control syntax and inline HTML -->
    <rule ref="PSR12.ControlStructures.ControlStructureSpacing">
        <exclude-pattern>*/tmpl/*</exclude-pattern>
        <exclude-pattern>*/layouts/*</exclude-pattern>
    </rule>
    <rule ref="Generic.Files.LineLength">
        <exclude-pattern>*/tmpl/*</exclude-pattern>
        <exclude-pattern>*/layouts/*</exclude-pattern>
    </rule>
</ruleset>

Typical commands, with their useful options:

composer install

# report, with rule names (-s) so a violation can be silenced precisely
vendor/bin/phpcs --standard=phpcs.xml -s

# summary per file, handy as a progress measure on an old code base
vendor/bin/phpcs --standard=phpcs.xml --report=summary

# fix automatically; review the diff before committing
vendor/bin/phpcbf --standard=phpcs.xml

# check only the files changed against the main branch (pre-commit or pull-request job)
git diff --name-only --diff-filter=ACMR origin/main -- '*.php' | xargs vendor/bin/phpcs --standard=phpcs.xml

Reading and silencing violations

A violation is identified by a dotted code such as PSR12.Files.FileHeader.SpacingAfterBlock. When a rule cannot be satisfied, for example in a method that must keep a legacy underscore name to override a core signature, silence the specific code for the specific lines, never the whole file:

// phpcs:disable PSR2.Classes.PropertyDeclaration.Underscore
protected $_legacyOptions = [];
// phpcs:enable PSR2.Classes.PropertyDeclaration.Underscore

phpcbf resolves most, not all, violations. The Manual says to run the checker again afterwards and fix the rest by hand. Always commit the formatting change separately from functional changes so that git blame stays useful, and list the commit in a .git-blame-ignore-revs file.

PHP-CS-Fixer

PHPCS only reports and does limited fixing. PHP-CS-Fixer rewrites code to a rule set and understands newer PHP syntax well. The core uses both: ruleset.xml for PHPCS and .php-cs-fixer.dist.php as the enforced configuration in its CI job, where it runs php-cs-fixer fix -vvv --diff, and the composer.json of the core defines a csfixer script that runs the same command in dry-run mode. Pull requests to joomla-cms are checked by that job.

The core configuration

The .php-cs-fixer.dist.php of 6.2-dev has these characteristics:

  • the rule set is @PSR12 with risky rules allowed (setRiskyAllowed(true)), the cache disabled, and the Finder excluding administrator/cache, administrator/logs, node_modules, libraries/php-encryption, libraries/phpass and the generated configuration.php

  • the extra rules include short array syntax, trailing commas in multi-line arrays only, aligned => and = in consecutive lines, the No break comment text for switch fall-through, removal of unused imports, imports ordered alphabetically (classes, then functions, then constants), no useless else, native_function_invocation for compiler-optimised functions, nullable_type_declaration_for_default_null_value, merged isset and unset calls, and no useless sprintf

  • classes of the global namespace are not imported: global_namespace_import is configured with import_classes, import_constants and import_functions set to false, which is why core code writes \stdClass and \RuntimeException with a leading backslash

  • a RuleCustomisationPolicyInterface implementation (JoomlaPolicy) turns off a list of layout-sensitive rules for tmpl, layouts, atum, cassiopeia and cassiopeia_extended files

The alignment rule and the global-namespace rule are the two that surprise newcomers: a PHPCS-clean file can still be rewritten by the fixer, so a project that enables both tools must pick compatible settings or run only one of them as the authority.

PHP-CS-Fixer for an extension

The following configuration reproduces the core approach for the Tallpine package, including the layout carve-out. It requires PHP-CS-Fixer 3.x with support for rule customisation policies:

<?php

declare(strict_types=1);

use PhpCsFixer\Config;
use PhpCsFixer\Config\RuleCustomisationPolicyInterface;
use PhpCsFixer\Finder;

$finder = (new Finder())
    ->in([__DIR__ . '/com_tallpine', __DIR__ . '/mod_tallpine_nexthikes', __DIR__ . '/plugins'])
    ->exclude(['node_modules', 'vendor', 'media']);

final class TallpinePolicy implements RuleCustomisationPolicyInterface
{
    public function getPolicyVersionForCache(): string
    {
        return hash_file('xxh128', __FILE__);
    }

    public function getRuleCustomisers(): array
    {
        $layoutOnly = [
            'binary_operator_spaces',
            'braces_position',
            'native_function_invocation',
            'no_unneeded_control_parentheses',
            'single_space_around_construct',
            'statement_indentation',
            'trailing_comma_in_multiline',
        ];

        $customisers = [];
        foreach ($layoutOnly as $rule) {
            $customisers[$rule] = static fn (\SplFileInfo $file): bool => !str_contains($file->getPathname(), '/tmpl/')
                && !str_contains($file->getPathname(), '/layouts/');
        }

        return $customisers;
    }
}

return (new Config())
    ->setRiskyAllowed(true)
    ->setUsingCache(true)
    ->setRules([
        '@PSR12' => true,
        'array_syntax' => ['syntax' => 'short'],
        'trailing_comma_in_multiline' => ['elements' => ['arrays']],
        'no_unused_imports' => true,
        'ordered_imports' => ['imports_order' => ['class', 'function', 'const'], 'sort_algorithm' => 'alpha'],
        'global_namespace_import' => ['import_classes' => false, 'import_constants' => false, 'import_functions' => false],
        'no_useless_else' => true,
        'native_function_invocation' => ['include' => ['@compiler_optimized']],
        'nullable_type_declaration_for_default_null_value' => true,
    ])
    ->setRuleCustomisationPolicy(new TallpinePolicy())
    ->setFinder($finder);
# list what would change, with a diff
vendor/bin/php-cs-fixer fix -vvv --dry-run --diff

# apply the fixes
vendor/bin/php-cs-fixer fix

# one file only: faster while editing
vendor/bin/php-cs-fixer fix com_tallpine/admin/src/Model/TrailsModel.php

Source: joomla-cms: .php-cs-fixer.dist.php (usage comment at the top of the file)

Two cautions apply. First, risky rules such as native_function_invocation can change behaviour when a function is redefined in a namespace, so run the unit tests after the first mass fix. Second, a CI job that fixes and pushes commits (the core does this only for forks, on push events, with a [skip ci] message) needs a token with write access, which is a credential to protect; for an extension prefer a read-only dry-run job that fails the build.

PHPStan

What it adds to a Joomla extension

PHPStan builds a model of every class it can see and checks each call against it. For Joomla extensions it is valuable for three reasons: it catches typos in calls to the large Joomla\CMS API, it flags types that are inconsistent after the move from CMSObject to stdClass and from setError() to exceptions (see Error handling and legacy APIs), and with the deprecation rules it lists the calls that will break on the next major version. The legacy J class aliases (JFactory, JText and about 450 others) are available in Joomla 6 only through the backward-compatibility plugin, so PHPStan reports them as unknown classes; that is the desired signal.

How the core configures PHPStan

The phpstan.neon of 6.2-dev includes a generated phpstan-baseline.neon, the deprecation rules and a Joomla-specific build/phpstan/phpstan.neon. It sets level: 0, phpVersion: 80300, scans libraries/src, administrator, components, installation and plugins, and lists ignoreErrors patterns for protected property and method access that the MVC classes use through magic. The Joomla-specific file registers the bootstrap file, stubs (AdminModel, Language, CMSApplication), universalObjectCratesClasses (for CMSObject, Table, CategoryNode and similar dynamic-property classes), a list of methods that terminate the request, and dynamic return-type extensions for the dependency-injection container, ExtensionManagerInterface and bootComponent(). The core level is deliberately low and ratcheted with a baseline; the CI job runs phpstan --error-format=github and regenerates the baseline.

The core’s dynamic return-type extensions live in the core repository (build/phpstan). They are not published as a separate Composer package, so an extension repository cannot simply require them; it receives the same effect only by analysing against a Joomla checkout and adding its own stubs where needed.

PHPStan for the Tallpine package

An extension repository does not contain Joomla, so PHPStan must be told where the Joomla classes are. The most robust method is to point scanDirectories at a Joomla 6.2 installation (the Docker Compose stack of Local development with Docker has one) through an environment variable, so no path is committed:

# phpstan.neon (NEON syntax; tabs or spaces, consistently)
includes:
    - phpstan-baseline.neon
    - vendor/phpstan/phpstan-deprecation-rules/rules.neon

parameters:
    level: 5
    phpVersion: 80300
    paths:
        - com_tallpine
        - mod_tallpine_nexthikes
        - plugins
    excludePaths:
        - com_tallpine/admin/tmpl
        - com_tallpine/site/tmpl
    scanDirectories:
        - %env.JOOMLA_ROOT%/libraries/src
        - %env.JOOMLA_ROOT%/libraries/vendor/joomla
        - %env.JOOMLA_ROOT%/administrator/components
        - %env.JOOMLA_ROOT%/components
        - %env.JOOMLA_ROOT%/plugins
    scanFiles:
        - %env.JOOMLA_ROOT%/libraries/namespacemap.php
    treatPhpDocTypesAsCertain: false
    reportUnmatchedIgnoredErrors: false

The file is written as YAML-like NEON; the [source,yaml] highlighting is only an approximation. Generate the baseline once, then require that it never grows:

export JOOMLA_ROOT=/var/www/html        # the Joomla 6.2 installation used for analysis

vendor/bin/phpstan analyse --memory-limit=1G

# accept all current errors as the starting point (creates phpstan-baseline.neon)
vendor/bin/phpstan analyse --generate-baseline --memory-limit=1G

# ratchet: raise the level one step at a time and fix or baseline what appears
sed -i.bak 's/level: 5/level: 6/' phpstan.neon && vendor/bin/phpstan analyse

Practical advice for the first run:

  • Start at level 0 or 1 on an existing extension, as the core does, and raise it once the baseline is stable. Level 5 is a reasonable target for new code; levels 6 to 9 require full type declarations on properties and parameters.

  • Analyse tmpl files only if you want to: they depend on variables injected by the view ($this), which PHPStan cannot see without help. Excluding them, as above, removes the noise; the view class is analysed instead.

  • The view object, the model and the table are accessed through magic and properties set at run time. The core handles that with ignoreErrors patterns and universalObjectCratesClasses; copy only the patterns your code actually triggers, and prefer typed properties and @property annotations on your own classes.

  • A small stub file can describe Joomla methods whose return type is too loose, for example an AdminModel::getItem() that returns stdClass in Joomla 6. List it under stubFiles.

Finding deprecated calls before Joomla 7

With phpstan-deprecation-rules enabled, every call to a method or class that carries a @deprecated tag in the Joomla source is reported. Joomla’s backward-compatibility policy removes deprecated code in the second major version after deprecation, so code flagged now is a guaranteed to-do list for the next major releases. Run this analysis against each new Joomla minor in CI, not only against the version you target, so the report is ready when you raise the minimum version. Typical findings in a 2023-era extension are Factory::getApplication() used where an injected application is expected, Factory::getDbo() instead of an injected DatabaseInterface, $this->setError() and getError() on models, and CMSObject return values; the replacements are described in Database API and Error handling and legacy APIs.

JavaScript, CSS and media sources

Joomla treats the media folder as build output. Hand-written sources live under media_source in the core and are compiled into media/ by the build in package.json; the PHP rules exclude media, and the JavaScript and Stylelint rules target the sources. An extension with its own web assets (the Tallpine trail map script, the SCSS of the module) should follow the same split, then register the compiled files in joomla.asset.json (see Web Asset Manager).

ESLint

The core runs ESLint 9 with the flat configuration build/eslint.config.mjs, using the @stylistic/eslint-plugin (customize preset with arrow parentheses always on, 1tbs brace style, single quotes, semicolons required) and eslint-plugin-vue for the Vue single-file components of the administrator. Files ending in .es5.js are ignored, the max-statements-per-line rule is off, and quotes is enforced as single with avoidEscape. The package script is:

{
  "scripts": {
    "lint:js": "eslint --config build/eslint.config.mjs --max-warnings 0 build/ media_source/"
  }
}

A Tallpine configuration derived from it:

// eslint.config.mjs
import { defineConfig } from 'eslint/config';
import stylistic from '@stylistic/eslint-plugin';

export default defineConfig([
  stylistic.configs.customize({
    arrowParens: true,
    braceStyle: '1tbs',
    quoteProps: 'as-needed',
    quotes: 'single',
    semi: true,
  }),
  {
    ignores: ['**/*.min.js', '**/*.es5.js', 'media/**'],
  },
  {
    rules: {
      '@stylistic/quotes': ['error', 'single', { avoidEscape: true }],
    },
  },
]);

Joomla 4.0 to 5.x core code used the Airbnb-based .eslintrc configuration; the move to the flat config and the stylistic plugin follows ESLint 9, which no longer reads .eslintrc files by default. Tutorials that show .eslintrc.json and eslint-config-airbnb-base for Joomla are therefore out of date for the current core.

Stylelint

The core runs Stylelint 16 with stylelint-scss, stylelint-order and @stylistic/stylelint-plugin, the postcss-scss custom syntax, and rules such as color-named: never, color-hex-length: short, max-nesting-depth: 4, selector-max-compound-selectors: 5, no duplicate properties, no empty blocks, and a fixed order/properties-order that places positioning properties first and typography last. The script covers the SCSS and CSS of the administrator and site templates, the installation template and media_source:

{
  "scripts": {
    "lint:css": "stylelint --config .stylelintrc.json \"com_tallpine/media_source/**/*.scss\" \"mod_tallpine_nexthikes/media_source/**/*.scss\""
  },
  "devDependencies": {
    "eslint": "^9.39.0",
    "@stylistic/eslint-plugin": "^5.10.0",
    "stylelint": "^16.26.0",
    "stylelint-config-standard": "^39.0.0",
    "stylelint-order": "^7.0.0",
    "stylelint-scss": "^6.14.0",
    "@stylistic/stylelint-plugin": "^4.0.0",
    "postcss-scss": "^4.0.9"
  }
}
{
  "plugins": ["stylelint-scss", "stylelint-order", "@stylistic/stylelint-plugin"],
  "customSyntax": "postcss-scss",
  "rules": {
    "color-named": "never",
    "color-hex-length": "short",
    "color-no-invalid-hex": true,
    "declaration-block-no-duplicate-properties": true,
    "block-no-empty": true,
    "max-nesting-depth": 4,
    "selector-max-compound-selectors": 5
  }
}

Both linters need Node.js 20 or later (the core engines field requires Node 20 and npm 10.1). Run them with npm run lint:js and npm run lint:css. The core CI runs lint:js, lint:testjs and lint:css in a matrix; the extra lint:testjs target lints the Cypress system tests with build/eslint-tests.mjs, which is the place to apply the same rules to the end-to-end tests described in Testing extensions.

How the pieces fit together

The checks form a pipeline from cheapest to most expensive. Fail fast on style, then on types, then on tests:

flowchart LR E[".editorconfig<br/>in the editor"] --> C[PHPCS / PHPCBF<br/>phpcs.xml] C --> F[PHP-CS-Fixer<br/>dry run with diff] F --> S[PHPStan<br/>level + baseline +<br/>deprecation rules] S --> T[PHPUnit<br/>unit tests] J[ESLint and Stylelint<br/>media_source] --> B[Build media/<br/>and package zip] T --> B

A pre-commit hook can run PHPCBF and ESLint on staged files, and the CI job runs everything again, because hooks can be skipped. In an editor, enable the PHPCS and PHP-CS-Fixer integrations of your IDE with the same configuration files, as the Manual recommends for the standard.

Common errors and pitfalls

  • phpcs: command not found in a Joomla checkout. Joomla sets Composer’s vendor-dir to libraries/vendor, so use ./libraries/vendor/bin/phpcs, or composer run phpcs.

  • The trailing . is missing. Without it the core command checks nothing or reports a usage error; keep the path argument, or define <file> entries in your ruleset.

  • Checking the whole Joomla tree instead of your extension. The core command scans every installed extension, so a clean run says little about your code. Point the tools at your own folders.

  • PHPCS and PHP-CS-Fixer fight each other. Alignment of => and the global_namespace_import choice are typical causes. Let the fixer be the authority for layout and use PHPCS for the rules the fixer lacks, or choose only one.

  • Layout files rewritten into invalid markup. Apply the layout carve-out to tmpl and layouts for fixer rules, and exclude them from line-length checks.

  • PHPStan cannot find Joomla\CMS classes. The extension repository has no Joomla; set JOOMLA_ROOT and scanDirectories, and add the extension namespaces to autoload-dev. A missing namespacemap.php makes the Joomla\Component and Joomla\Plugin classes unknown.

  • Hundreds of PHPStan errors on the first run. Generate a baseline and ratchet the level; do not disable the tool.

  • Linting built files. ESLint on media/ reports the output of the bundler. Lint media_source/ and ignore media/.

  • Copying the 560-character line limit. The core value is a temporary migration allowance, not a recommendation.

  • Legacy underscore property exemptions. The core exemption list is for old classes; do not add new underscored properties to qualify for it.

  • Version drift. PHPCS is pinned to 3.13.x in the core, while PHPCSStandards has moved ahead; check that the standard you reference exists in the version you install, and pin versions in composer.lock so CI and laptops agree.

Security implications

Style and type checks are not substitutes for the security practices of Security in extensions, but they support them:

  • PHPStan finds null dereferences and wrong types that turn into fatal errors, which expose paths and configuration on a misconfigured server, and type juggling bugs in access checks.

  • The deprecation rules show uses of removed filtering and input APIs before an upgrade silently changes behaviour.

  • Dev tools are code that runs on developer machines and in CI. Pin versions with a lock file, run composer audit and npm audit in CI, and never ship vendor, node_modules, tests, phpcs.xml or source maps in the extension zip.

  • A CI job that auto-commits fixes needs write credentials; keep tokens in the CI secret store and restrict them to the repository. Prefer a dry-run job that fails the build.

  • Do not paste real configuration.php values, secrets or database credentials into analysis fixtures, baselines or bug reports created from tool output.

Version notes

  • Joomla 4.2: PSR-12 becomes the project coding standard.

  • Joomla 5.x: the coding standard is unchanged; PHP 8.1 and later features appear in core code, and extensions targeting both 4.x and 5.x may keep the older Factory calls that PHPStan will flag on 6.

  • Joomla 6.0: minimum PHP 8.3 and removal of the legacy aliases and classes listed in Upgrading Joomla 5 to 6, which PHPStan reports as unknown classes unless the backward-compatibility plugin classes are scanned. Analyse your code once against the plain 6.x tree to see what fails without it.

  • Joomla 6.2: core pins PHPUnit 12.5, PHPStan 2.1.40 and PHP-CS-Fixer 3.94.2 as dev tools and runs its CI in a PHP 8.5 image, while the platform requirement stays PHP 8.3.0.

Where older books and tutorials are outdated

Older books such as Developing Extensions for Joomla! 5 (Cámara Mora, 2023) and Joomla! 4 Masterclass (Marzo, 2023) do not cover static analysis or code-quality tooling for extensions, and the Manual’s own style page is still a stub. Material from the legacy wiki, such as the Joomla CodeSniffer page, belongs to the era before PSR-12 and describes a Joomla-specific standard that the core no longer uses. Tutorials that quote Airbnb-based ESLint configurations, .eslintrc.json files, or the Joomla 3 style (tabs, braces on the same line as the class, JFactory calls) should be replaced by the configuration of the current joomla-cms branch. The wider list of outdated claims is collected in Where older books and tutorials are outdated.