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
usestatement per imported class, grouped and ordered, one blank line after the import block -
StudlyCapsclass names,camelCasemethod names,UPPER_CASEclass constants -
visibility declared on every property, method and constant; the
abstractandfinalkeywords before visibility,staticafter 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
PSR12with a singlerule refline. -
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 generatedconfiguration.php. -
Raises the line-length limit of
Generic.Files.LineLengthto 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, theinstallationscripts and thelibrariesloader 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
Source: joomla-cms: .editorconfig
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"]
}
}
Source: joomla-cms: composer.json
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>
Source: joomla-cms: ruleset.xml
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
Source: PHP_CodeSniffer wiki: Usage
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
@PSR12with risky rules allowed (setRiskyAllowed(true)), the cache disabled, and theFinderexcludingadministrator/cache,administrator/logs,node_modules,libraries/php-encryption,libraries/phpassand the generatedconfiguration.php -
the extra rules include short array syntax, trailing commas in multi-line arrays only, aligned
=>and=in consecutive lines, theNo breakcomment text forswitchfall-through, removal of unused imports, imports ordered alphabetically (classes, then functions, then constants), no uselesselse,native_function_invocationfor compiler-optimised functions,nullable_type_declaration_for_default_null_value, mergedissetandunsetcalls, and no uselesssprintf -
classes of the global namespace are not imported:
global_namespace_importis configured withimport_classes,import_constantsandimport_functionsset tofalse, which is why core code writes\stdClassand\RuntimeExceptionwith a leading backslash -
a
RuleCustomisationPolicyInterfaceimplementation (JoomlaPolicy) turns off a list of layout-sensitive rules fortmpl,layouts,atum,cassiopeiaandcassiopeia_extendedfiles
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.
Source: joomla-cms: phpstan.neon
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
Source: PHPStan: Configuration Reference
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
Source: PHPStan: Baseline
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
tmplfiles 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
ignoreErrorspatterns anduniversalObjectCratesClasses; copy only the patterns your code actually triggers, and prefer typed properties and@propertyannotations on your own classes. -
A small stub file can describe Joomla methods whose return type is too loose, for example an
AdminModel::getItem()that returnsstdClassin Joomla 6. List it understubFiles.
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/"
}
}
Source: joomla-cms: package.json
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"
}
}
Source: joomla-cms: package.json
{
"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:
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 foundin a Joomla checkout. Joomla sets Composer’svendor-dirtolibraries/vendor, so use./libraries/vendor/bin/phpcs, orcomposer 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 theglobal_namespace_importchoice 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
tmplandlayoutsfor fixer rules, and exclude them from line-length checks. -
PHPStan cannot find
Joomla\CMSclasses. The extension repository has no Joomla; setJOOMLA_ROOTandscanDirectories, and add the extension namespaces toautoload-dev. A missingnamespacemap.phpmakes theJoomla\ComponentandJoomla\Pluginclasses 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. Lintmedia_source/and ignoremedia/. -
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.lockso 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
nulldereferences 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 auditandnpm auditin CI, and never shipvendor,node_modules, tests,phpcs.xmlor 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.phpvalues, 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
Factorycalls 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.