Search by

mike-bronner / clean-code

mikebronner

PHPCS ruleset and custom sniffs enforcing the clean-code standards defined at mikebronner.dev/clean-code.

Package info

github.com/mike-bronner/clean-code

Type:phpcodesniffer-standard

pkg:composer/mike-bronner/clean-code

Statistics

Installs: 26

Dependents: 1

Suggesters: 0

Stars: 1

Open Issues: 0

0.1.1 2026-09-24 17:23 UTC

This package is auto-updated.

Last update: 2026-09-24 17:28:16 UTC


README

PHPCS linter rules for all coding standards defined in https://mikebronner.dev/clean-code.

Installation

Composer 2.2 and later will not run a plugin the consuming project has not allowed, and the codesniffer installer is a plugin. Allow it first:

composer config allow-plugins.dealerdirect/phpcodesniffer-composer-installer true
composer require --dev mike-bronner/clean-code

Or add it to composer.json by hand:

{
    "config": {
        "allow-plugins": {
            "dealerdirect/phpcodesniffer-composer-installer": true
        }
    }
}

Without that opt-in the installer never runs, PHP_CodeSniffer's installed_paths is never written, and the standard does not register — phpcs then reports the standard as not installed, with nothing pointing at the plugin as the cause. It is the most common reason an install appears to do nothing.

With the plugin allowed, vendor/bin/phpcs -i lists one standard from this package, CleanCode. It is the whole rule set: the custom sniffs, the PSR and Slevomat wiring, the PHP version pin and Blade support.

Usage

Reference the standard by name from your project's own phpcs.xml (or phpcs.xml.dist):

<rule ref="CleanCode"/>

Or run it directly:

vendor/bin/phpcs --standard=CleanCode src/

The name works from anywhere in your project and survives a custom Composer vendor-dir, which a path reference does not. It is also the prefix on every code the custom sniffs report, so CleanCode.Naming.ShortVariable in a report and CleanCode in your config are the same word.

Contributing

See CONTRIBUTING.md for the package layout, how a new sniff or standard plugs into CleanCode/ruleset.xml, the test-fixture contract, and how to add its Pest test.

Standards

Each standard is documented under docs/standards/. Every entry below carries the tier assigned in that document's Enforceability section, and summarizes how the standard is enforced.

  • Tier 1 and Tier 2 standards are enforced automatically, by rules wired into the CleanCode standard: a bundled standard, an existing PHPCS or Slevomat rule (configured where needed), a custom sniff, or a combination of these.
  • Tier 3 standards are the ones a token-based sniff cannot verify. They are enforced by code review and developer discipline rather than a PHPCS rule. A few carry a rule over one narrow slice that is token-visible — the tier still describes the standard's core, not that slice.

Where a rule covers only part of a standard, that standard's document records what stays with code review.

  • Code Style: Linters (config & no auto-formatter) — Tier 3, custom sniff CleanCode.CodeStyle.NoFormatterDirectives over one narrow slice: flags every comment carrying an auto-formatter directive (@formatter:off, @formatter:on, prettier-ignore by default, replaceable through a directives property), detection-only; editor configuration, formatter config files, and a formatter run that left no directive behind stay with code review (#51 + #143)
  • Debt: Technical Debt — Tier 3, not statically enforceable
  • Code Style: Linters (config & no auto-formatter) — Tier 3, not statically enforceable
  • Debt: Technical Debt — Tier 3, core not statically enforceable, with one narrow slice enforced: self-declared debt markers warn, via Generic.Commenting.Todo and Generic.Commenting.Fixme (the latter lowered to a warning) plus custom sniff CleanCode.Commenting.DebtMarkers for the HACK and XXX keywords core ships nothing for, detection-only; recognizing undeclared debt, judging whether a construct makes future change costlier, and paying debt down promptly all stay with code review (#24 + #138)
  • Boy Scout Rule — Tier 3, not statically enforceable
  • Don't Optimize Early — Tier 3, not statically enforceable
  • Arrays: Array Accessors (data_get) — Tier 2, custom sniff CleanCode.Arrays.ArrayAccessors: flags direct element ($array['key']) and property ($object->property) reads once per accessor chain, leaving write-side access, existence checks, array literals, $this-rooted access, and method calls alone, detection-only (#33)
  • Arrays: Convert To Collection — Tier 2, custom sniff CleanCode.Arrays.ConvertToCollection: warns on calls to a configurable list of native array functions (array_map, array_filter, array_reduce by default), naming the Collection equivalent, and leaving method calls, static calls, declarations, and namespaced same-named functions alone, detection-only; whether a given manipulation reads better as a pipeline, and foreach accumulation, stay with code review (#30 + #165)
  • Arrays: Operator Spacing & Line Breaks — Tier 1, configured Squiz.WhiteSpace.OperatorSpacing + Squiz.Strings.ConcatenationSpacing (exactly one space each side, auto-fixable) plus custom sniffs CleanCode.Operators.NotOperatorSpacing (auto-fixable) and CleanCode.Operators.OperatorLineBreak (reporting only) (#35)
  • Blank Lines — Tier 1, custom sniff: CleanCode.WhiteSpace.BlankLines, auto-fixable (#43)
  • Classes: Class Naming — Tier 2, custom sniff CleanCode.Naming.RedundantNamespaceSuffix, error severity, detection-only: flags a class, interface, trait, or enum under an App\ namespace whose name ends in a suffix one of its own namespace segments already says (App\Services\BillingService, App\Http\Controllers\UserController), matching every ancestor segment and its singular forms at a PascalCase word boundary, case-insensitively; no per-folder exemption, so Laravel's generated Controller/Request/Form names are flagged too and the fuller name belongs in a use … as alias at the call site. Renaming a type rewrites every reference to it, so there is no fixer (#27)
  • Classes: Contracts (Interfaces) — Tier 3, no rule: whether a class needs a contract depends on how it is instantiated and consumed across the codebase, which a single-file token stream cannot see, so the whole standard stays with code review (#10)
  • Classes: Introspection / Type Casting — Tier 2, custom sniff CleanCode.Classes.DisallowTypeIntrospection: flags instanceof and get_class()/get_debug_type()/gettype()/is_a()/is_subclass_of() where they decide a branch (if/elseif/while/switch/match/ternary), detection-only (#73)
  • Classes: No Statics — Tier 2, custom sniff CleanCode.Classes.DisallowStaticMembers: flags static method/property declarations in any class/interface/trait/enum, detection-only. A member a resolvable ancestor declares static and non-private is skipped, because PHP refuses to load a class that drops the keyword — a Laravel facade's getFacadeAccessor() is the common case (#19)
  • Clear Code: One Thought Per Line — Tier 2, custom sniff CleanCode.ClearCode.OneThoughtPerLine: one access operator per chain per line, auto-fixable. An operator still held by an unclosed (, [, or short-array opener is exempt, so argument lists, array literals and statement conditions stay on one line; the exemption stops at a brace, so a closure body's chains are still counted (#7)
  • Code Style: Industry Standards (PSR1/2/12) — Tier 1, bundled standard: PHPCS PSR12 (includes PSR1; supersedes PSR2) wired into the CleanCode standard (#49)
  • Code Style: Multiline Strings (HEREDOC) — Tier 2, custom sniff CleanCode.Strings.MultilineStrings, which owns length only: flags multi-line quoted strings (auto-fixed to a HEREDOC, whatever the source was quoted with) and quoted-string concatenation whose value carries more than maximumLines lines (default 3, detection-only). Lines of text, never lines of source — a sentence wrapped across four source lines is one line of text, and a HEREDOC cannot express it without either breaking the 120-character limit or putting real newlines into the value, so counting the source made the rule report what no fix could satisfy. Source layout is already governed by the 100-character limit and the leading-operator rule. Embedded languages belong to CleanCode.Strings.RequireHeredocForStructuredText at any length, so the two hold disjoint slices (#53)
  • Collections: Only Use Collection Methods — Tier 2, custom sniff CleanCode.Collections.OnlyUseCollectionMethods: flags generic PHP array/string functions applied to a Collection, auto-fixable for the unambiguous 1:1 swaps (count(), array_sum()) on a provably-typed receiver (#28)
  • Conditionals: Avoid Conditionals — Tier 2, custom sniff CleanCode.Conditionals.AvoidConditionals: one warning per if/elseif/ternary/switch, detection only, plus SlevomatCodingStandard.ControlStructures.UselessIfConditionWithReturn lowered to warning for the auto-fixable boolean-return if; whether a branch was avoidable stays with code review (#12)
  • Conditionals: Combine Where Possible — Tier 2, custom sniff CleanCode.Conditionals.CombinableConditions: one warning per participating branch, for adjacent branches of one if/elseif chain with identical bodies and for adjacent separate plain if statements whose identical bodies unconditionally exit; detection only, since whether the combined condition reads better stays with code review (#181)
  • Conditionals: Mapping Arrays — Tier 2, custom sniff CleanCode.Conditionals.MappingArrayCandidate: one warning per if/elseif chain whose conditions all compare the same variable against scalar literals and whose branches only produce a value, detection only, with a configurable minimumBranches threshold; whether a mapping array reads better stays with code review (#163)
  • Conditionals: No else or elseif — Tier 1, custom sniff CleanCode.Conditionals.DisallowElse: reports every else (.Found) and every elseif, including the two-word else if (.ElseIfFound), one violation per keyword; auto-fixable where every preceding branch already ends in a jump statement and the layout is canonical, reported and left alone everywhere else (#14)
  • Conditionals: No Inline If-Statements — Tier 1, existing sniff: Generic.ControlStructures.InlineControlStructure, auto-fixable (#9)
  • Conditionals: One Condition Per Line — Tier 2, custom sniff: auto-fixable condition-layout enforcement (#17)
  • Conditionals: Ternary Conditionals — Tier 1, Slevomat ControlStructures.RequireTernaryOperator for the if/else that only assigns or returns (auto-fixable) plus custom sniff CleanCode.Conditionals.DisallowNestedTernary, which reports every nested ternary at the inner operator, detection only — unfolding one needs a variable or method name, which no fixer can invent (#20)
  • Constructors: No Logic in Constructors — Tier 2, custom sniff CleanCode.Constructors.NoLogic: flags every non-assignment statement in a __construct() body, detection-only (#40)
  • Constructors: Primary + Named Constructors — Tier 2, custom sniffs: CleanCode.Constructors.PrimaryConstructorDelegation warns on a named constructor — a static method returning self, static, or the declaring class — whose body neither instantiates that class nor hands off to another of its static methods, detection-only (#184); CleanCode.Constructors.DisallowCombinedConstructor warns when a __construct body branches on a construction-mode signal — a boolean mode flag (ModeFlag), a parameter's runtime type (TypeSwitch), or the argument list the caller supplied (ArgumentCount) — skipping guard clauses and coalesce defaults, detection-only (#193)
  • Constructors: Property Promotion — Tier 1, Slevomat Classes.RequireConstructorPropertyPromotion, auto-fixable (#47)
  • Controllers: No Business Logic — Tier 3, code review only: "business logic" is a semantic judgement and the prescribed extraction to Form Request / Response classes is cross-file, so no single-file sniff can decide it; the one token-visible slice (non-RESTful public method names) is tracked as follow-up sniff issue #141 (#48)
  • Controllers: Route Model Binding — Tier 2, custom sniff CleanCode.Controllers.ManualModelResolution: warns when a public *Controller method resolves a model by hand (Model::find($id) / findOrFail($id)) from one of its own parameters instead of type-hinting the model and letting route-model binding resolve it, detection-only (#50, superseding #169)
  • Controllers: No Business Logic — Tier 2, custom sniff CleanCode.Controllers.NoCustomActions: warns on a public method declared in a *Controller class that is not one of the seven Laravel resource actions, __construct, __invoke, or a name in the configurable allowedMethods allowlist, detection-only (#141); the rest of the standard stays with code review — "business logic" is a semantic judgement and the prescribed extraction to Form Request / Response classes is cross-file, so no single-file sniff can decide it (#48)
  • Dependency Injection — Tier 2, custom sniff CleanCode.Classes.DisallowConstructorInstantiation: warns once per new inside the body of a __construct a class-like scope holds, skipping whatever a throw raises and any nested declaration, detection-only; whether a collaborator should be injected is an architectural judgement no single-file token scan can make, so that half stays with code review (#72 + #176)
  • Exceptions — Tier 2, enforced via Slevomat rules: \Throwable-only catches + non-capturing catch, both auto-fixable (#63)
  • Indentation: Logical Groupings — Tier 2, custom sniff CleanCode.Indentation.LogicalGroupings: indents parenthesized condition groups one level deeper per nesting level, auto-fixable (#41)
  • Indentation: Methods (max 2 nesting levels) — Tier 2, custom sniff CleanCode.Metrics.MethodNestingLevel: flags control structures nested more than 2 levels deep, not auto-fixable (#36)
  • Indentation: Multi-Line Statements — Tier 1, custom sniff CleanCode.WhiteSpace.MultiLineStatementIndent: every line after the first of a multi-line statement is indented exactly one level in from the line its construct opens on, auto-fixable (#38)
  • Line Length — Tier 1, configured rule: Generic.Files.LineLength warns above 100 characters, errors above 120 (#3)
  • Methods: Declared Parameters — Tier 2, custom sniff CleanCode.Methods.DeclaredParameters: flags func_get_args() / func_get_arg() / func_num_args() calls, exempting magic methods, detection-only (#69)
  • Livewire: Components — Tier 2, custom sniff CleanCode.Livewire.ComponentMarkup: reads Blade views (registered via the blade.php extension) and reports framework attributes on a component's root element, a <livewire:…> tag in a loop without wire:key, and adjacent components missing a key-matched <template> wrapper; heuristic and detection-only, so root-element count and wire:key uniqueness stay with code review (#46)
  • Methods: No Null Arguments — Tier 2, custom sniff CleanCode.Methods.NoNullArguments: flags a literal null passed positionally into an optional parameter, auto-fixed to a named argument wherever the file proves which declaration the call reaches (#71)
  • Methods: Type Hints — Tier 1, Slevomat TypeHints.ParameterTypeHint + TypeHints.ReturnTypeHint, configured in the CleanCode standard, plus the custom CleanCode.TypeHints.InferredReturnType, which carries the fixer Slevomat cannot: Slevomat writes a return type only from a @return annotation, while this one writes it wherever it is provable from the source — a type a resolvable ancestor already declares (read by reflection), a body whose returns are all literals, one returning $this, or one returning a typed parameter. It stays silent on everything else, because a guessed return type is a runtime TypeError rather than a lint finding, and cedes the magic-method, @return-annotated and returns-nothing cases to Slevomat outright (#70)
  • Models: Eager Loading — Tier 2, two custom sniffs, both detection-only: CleanCode.Models.RequireLazyLoadingPrevention warns when the application service provider never calls Model::preventLazyLoading() (or Model::shouldBeStrict()), leaving Laravel's runtime lazy-loading safety check switched off (#154); CleanCode.Models.DisallowAlwaysOnEagerLoading warns on a populated $with property in a class extending a model-shaped parent, the always-on eager loading the standard's first rule prohibits (#153)
  • Models: Organization (member ordering) — Tier 2, custom sniff CleanCode.Models.MemberOrdering: in a class extending a model-shaped parent — named or anonymous, each ordered against itself — errors on a trait use, property, relationship method, getter/setter, or other method that is out of alphabetical order, on a property whose visibility group precedes the previous property's, and on a use statement declaring several traits; detection-only, because reordering members would move doc blocks and comments bound to a declaration by nothing but adjacency (#75)
  • Models: Naming Conventions — Tier 2, custom sniff CleanCode.Naming.ModelNamingConventions: yes/no prefixes on boolean model properties/methods, find/get prefixes keyed off the return type, and legacy getXAttribute() accessors, detection-only (#44)
  • Models: Persistence Methods (Repository Pattern) — Tier 2, custom sniff CleanCode.Models.DisallowExternalPersistenceCalls: warns on generic CRUD calls (create/delete/save/update) on any receiver other than $this, detection-only (#37)
  • Models: Relationship Properties — Tier 2, custom sniff CleanCode.Models.DisallowChainedPropertyFetch: errors on chained property-fetch access, not auto-fixable (#42)
  • Naming: Casing Conventions — Tier 1, existing sniffs: camelCase variables/properties/methods, PascalCase classes (#22)
  • No Dead Code — Tier 2, Squiz + Slevomat rules for commented-out code, unused parameters, and unused imports, plus a custom unused-private-elements sniff (#29)
  • Operators: Active — Tier 1, four cooperating sniffs, each owning a disjoint slice of the operator list so no violation is reported twice: Squiz.WhiteSpace.OperatorSpacing for the assignment operators, Squiz.Strings.ConcatenationSpacing for ., and CleanCode.Operators.NotOperatorSpacing for ! (all three wired in for #35), plus the custom CleanCode.Operators.BooleanOperatorSpacing for the logical connectives &&, ||, and, or, xor — the only operators in the list nothing else already covered; all auto-fixable (#62)
  • Operators: Evaluative — Tier 1, custom sniff: newline-around-evaluative-operator detection, auto-fixable (#56)
  • Operators: Manipulative — Tier 2, three cooperating sniffs, each owning a disjoint slice of the operator list so no wrap is reported twice: CleanCode.Operators.OperatorLineBreak for ., && and || and CleanCode.Conditionals.OneConditionPerLine inside a control-structure condition (both already wired in, for #35 and #17), plus the custom CleanCode.Operators.ManipulationOperatorPlacement for the math (+ - * / % **) and bitwise (& | ^ << >>) groups — the only operators in the list nothing else already covered. A manipulation operator must lead a wrapped line, not trail it; inline usage, unary/reference forms and catch-clause type unions are untouched, and the new sniff auto-fixes its own half (#59)
  • Operators: Passive — Tier 1, custom sniff CleanCode.WhiteSpace.PassiveOperatorSpacing (identity, negation, error control, execution) plus existing sniffs for increment/decrement, ->, and [], all auto-fixable (#64)
  • Pattern: Don't Repeat Yourself (DRY) — Tier 2, custom sniff: repeated-block detection (#134)
  • Naming: Semantic naming principles — Tier 3, one custom sniff over a mostly review-owned standard: whether a name reveals its intent, misleads, or keeps one word per concept is a judgement about meaning that a single file's token stream cannot make, so the semantic core stays with code review (#18); the one token-catchable slice is carried by CleanCode.Naming.DisallowMagicNumbers, which warns on a bare numeric literal used outside a declaration site, where a named constant would make the concept searchable — detection-only, with a configurable ignoredNumbers list defaulting to 0, 1, -1 (#136)
  • Pattern: Don't Repeat Yourself (DRY) — Tier 2, custom sniff CleanCode.Pattern.AvoidDuplicateCodeBlocks: warns on every run of code lines in a file that repeats another run — each block reported at its own first line, naming every other block of that shape — comparing token types rather than token content so renamed variables and changed literals still match, above a configurable line threshold (default 5), detection-only; whether a flagged duplication has earned an abstraction, and duplicated knowledge that is not duplicated text, stay with code review (#134)
  • Pattern: Model-View-Controller (MVC) — Tier 3, code review: MVC is a recommended default, not a mandatory architecture, so no rule requires a feature to have a Model, View, or Controller class, and accepted alternatives — Livewire full-page and single-file components — are not violations; the one token-visible slice, RESTful public-method naming, applies only to a class already suffixed Controller and ships as CleanCode.Controllers.NoCustomActions under Controllers: No Business Logic (#167, closed as a duplicate of #141)
  • Pattern: Repository — Tier 3 for the semantic half only: whether persistence logic actually lives in the model or its attribute/query traits is a judgement about intent that no token-based sniff can verify, so that half stays with code review (#6); the complementary "no dedicated class outside the model implements the repository pattern" half is token-visible and is Tier 2, carried by the custom sniff CleanCode.Pattern.DisallowRepositoryClasses: it warns on a class, interface, trait, or enum whose declared name ends in Repository/RepositoryInterface or whose namespace carries a Repositories segment (both case-insensitive), reading declarations only — extends, implements, a trait use, an import and new are consumption sites and are never flagged — detection-only, a naming/namespace heuristic rather than a verification of the semantic standard (#126)
  • Policies: Secure Front- and Back-Ends — Tier 3, no rule: front-end restrictions live outside the linted token stream, and a missing back-end check is an absence a single-file sniff cannot distinguish from a guard placed in another file, so the whole standard stays with code review (#52)
  • Strings: Interpolation, quoting, HereDocs — Tier 2, custom sniffs CleanCode.Strings.*: concat→interpolation (auto-fixable for any chain whose operands all have an interpolated form, including member, index and call expressions; a grouping parenthesis or a binary-string prefix stays detection-only), HTML attribute double-quoting (auto-fixable), embedded-language-should-be-HereDoc at any length, covering HTML, XML, SQL, JSON, YAML, INI/config and markdown, each behind an anchored signal (detection-only), nested-quote escaping (auto-fixable), and NOWDOC→HEREDOC (auto-fixable — the two are the same construct and differ only in the quotes around the opening identifier, so the package keeps one; a backslash doubles and a $ is escaped, which covers {$ and ${ alike, while a " needs nothing) (#25)
  • Routes: Conventions (Do / Do Not) — Tier 2, one enforceable bullet at a time. Shipped: custom sniff CleanCode.Routes.DisallowNonResourceRoutes, which warns on a route registered with an HTTP-verb call (Route::get, post, put, patch, delete, options, any, match) instead of Route::resource()/Route::apiResource(), anchored at the verb token, confined to configurable route-path globs (routeFilePatterns), detection-only and warning-severity because the standard permits a rare special-action exception a sniff cannot recognise (#248). Still queued as focused follow-ups: closure route actions at error severity (#174) and non-invokable special-action controllers (#249). Whether the target controller is really RESTful, one-model-per-route, model-based naming, and model-unrelated routes need project-wide symbol resolution and stay with code review (#65)
  • Routes: Conventions (Do / Do Not) — Tier 2, three focused sniffs, one per enforceable bullet: closure route actions at error severity, custom sniff CleanCode.Routes.DisallowClosureRoutes (#174), plus heuristic warnings for non-resource verb routes (#248) and non-invokable special-action controllers (#249); one-model-per-route, model-based naming, and model-unrelated routes need project-wide symbol resolution and stay with code review (#65)
  • Testing: Development Process (TDD) — Tier 3, not statically enforceable: test-first order and the Red/Green/Refactor cycle are facts about the process, not the tokens, so the standard stays with code review (#57); two token-visible slices are queued as focused follow-ups (#128, #129)
  • Routes: Types (API / View) — Tier 2, custom sniff CleanCode.Routes.ApiControllerNamespace: requires a controller's API namespace segment and its API path segment to agree, in both directions, detection-only. A proxy for the written standard — the route-registration wording it stands in for, and the one-controller-per-model clause, stay with code review (#66)
  • Testing: Databases (SQLite caveats) — Tier 3, no rule: the standard forbids a pairing — SQLite as the test driver together with JSON columns, exact decimal maths, table-altering migrations, or raw date queries — and the driver half is declared in phpunit.xml/.env.testing/CI config that PHPCS never tokenizes, so no single-file sniff can join the two halves and no partial-enforcement slice survives (#58)
  • Testing: Development Process (TDD) — Tier 3 for the process: test-first order and the Red/Green/Refactor cycle are facts about the process, not the tokens, so that half stays with code review (#57); the classes-only slice is Tier 2, custom sniff CleanCode.Files.NoProceduralCode, scoped to src/+app/: any top-level statement outside a single class/interface/trait/enum declaration is an error, detection-only, and stricter than PSR1.Files.SideEffects (#129); the test-existence slice is Tier 2 as well, custom sniff CleanCode.Testing.RequireTestFile, which warns when a concrete class under a configured source directory has no companion test file — one glob() call per class against a configurable source-to-test mapping (sourceDirectories, testDirectory, testPathTemplate, excludePatterns), detection-only, exempting interfaces, traits, enums, anonymous and abstract classes, and enforcing test existence only, never test-first order or test quality (#128)
  • Testing: Guidelines — Tier 3 core, two partial rules: the standard turns on process and cross-file judgements (where testing started, whether every scenario has success and failure coverage, whether a mocked interface is one the team controls) that a single file's token stream does not carry, so the semantic core stays with code review (#54); the "only test public methods" slice is enforced by the custom sniff CleanCode.Testing.NoReflectionAccess, which warns on Reflection reaching a named non-public member inside a test file (new ReflectionMethod/ReflectionProperty, setAccessible, getMethod, getProperty, invoke, invokeArgs), detection-only and confined to configurable test-path globs (#145); the "do not mock classes you control" slice is enforced by the custom sniff CleanCode.Testing.NoFirstPartyMocks, which warns on a mock-creation call (createMock, createPartialMock, getMockBuilder, Mockery::mock/spy, Laravel's mock/partialMock/spy) whose class argument resolves into a configured first-party namespace root, also detection-only and test-path confined (#146)
  • Testing: Guidelines — Tier 3 core, one partial rule: the standard turns on process and cross-file judgements (where testing started, whether every scenario has success and failure coverage, whether a mocked interface is one the team controls) that a single file's token stream does not carry, so the semantic core stays with code review (#54); the "only test public methods" slice is enforced by the custom sniff CleanCode.Testing.NoReflectionAccess, which warns on Reflection reaching a named non-public member inside a test file (new ReflectionMethod/ReflectionProperty, setAccessible, getMethod, getProperty, invoke, invokeArgs), detection-only and confined to configurable test-path globs (#145); the mocking slice remains a focused sniff issue (#146)
  • Testing: Test Suites — Tier 3 core, four enforced slices: what each suite is for — a unit test's actual scope, whether a feature test stays off the internet at runtime, and the faked-feature ↔ unfaked-integration twin pairing — turns on runtime behaviour and cross-file judgement, so the core stays with code review (#60); the layout slice is carried by the custom sniff CleanCode.Testing.TestSuiteNamespace, which warns when a test class's declared suite namespace and its suite directory contradict each other, in both directions (NamespaceMismatch, DirectoryMismatch), reading both sides from the segment directly below a configurable test root and leaving a no-namespace class, STDIN, and non-test declarations alone — detection-only, since reconciling a mismatch means moving the file or renaming its namespace and only the project's layout says which; the content slice for the unit suite is carried by the custom sniff CleanCode.Testing.UnitTestExternalConcerns, which warns when a file under a configurable unitTestPath (default tests/Unit/) carries a database trait (DatabaseTrait), a facade fake() (FacadeFake), or an HTTP-kernel request call on the test case (HttpRequest) — token-visible external concerns that belong in tests/Feature/, matched on whole path segments anchored at the test root closest to the file, warning-severity, detection-only and suppressible per line (#148); the feature-suite content slice is carried by the custom sniff CleanCode.Testing.NoInternetTraversal, which warns on a raw internet-traversing primitive written in a feature test — curl_init, curl_exec, fsockopen, stream_socket_client, a file_get_contents whose filename argument — positional or named filename: — is a whole string literal (quoted, heredoc or nowdoc) naming http:// or https://, and a direct new GuzzleHttp\Client resolved through the file's own namespace and imports — leaving the sanctioned Http::fake() route alone and confined to configurable feature-path globs (featureTestPatterns), detection-only and warning-severity because a primitive is a strong signal rather than proof a request left the machine (#149); the integration-suite content slice is carried by the custom sniff CleanCode.Testing.NoHttpFakesInIntegrationTests (#150)
  • Type Hints and Return Types — Tier 1, Slevomat TypeHints.PropertyTypeHint (parameter/return hints for all callables are owned by Methods: Type Hints, #70), partly auto-fixable (#45)
  • Use Statements: No Unused Entries — Tier 1, Slevomat Namespaces.UnusedUses, auto-fixable (#68)
  • Use Statements: Sort Alphabetically — Tier 1, Slevomat Namespaces.AlphabeticallySortedUses plus DisallowGroupUse + MultipleUsesPerLine to close the group-use and comma-separated bypasses, auto-fixable for flat blocks (#67)
  • Pattern: SOLID — Tier 3 core; all five principles yielded a token-visible slice, and all five are enforced today: Single Responsibility by the seven shipped size/coupling sniffs (#80, #83, #87, #93, #96, #98, #114); Dependency Inversion by CleanCode.Classes.DisallowConstructorInstantiation (#72); Liskov Substitution by CleanCode.Pattern.ThrowOnlyMethodOverride, which warns on a method whose entire body is a single throw in a type that extends or implements — refused bequest — detection-only (#131); Interface Segregation by CleanCode.Pattern.TooManyInterfaceMethods, which warns on an interface declaring more than maxMethods (default 5) method signatures — counting only what the body declares, not what an extends list inherits — detection-only (#132); and Open-Closed by CleanCode.Conditionals.TypeDiscriminatorDispatch, which warns once per switch or if/elseif chain that dispatches on one type-discriminator read ($shape->type, $row['type']) across enough literal branches, at the switch keyword or the leading if, detection only, with a configurable minimumBranches threshold (default 3) (#324); whether a flagged dispatch should have been polymorphism, and the semantic core of all five, stay with code review (#5)

PHPMD rule coverage

The CleanCode standard also replicates PHPMD rules, so a project running this ruleset does not need to run phpmd separately for them. Each mapping is documented under docs/phpmd/.

  • Clear Code: Group Code By Concepts — Tier 3, not lintable; enforced via code review (partial blank-line heuristic noted, follow-up sniff issue left to a human)
  • Debt: Mental Debt — Tier 3, code review only; measurable sub-rules tracked separately
  • Clear Code: One Idea Per Statement — Tier 3, code review; partial enforcement via chained-assignment / assignment-in-condition sniffs (#157)
  • Clear Code: Encapsulate Each Concept in a Method — Tier 3 core, code review; the section-labelling-comment slice is enforced by the custom sniff CleanCode.ClearCode.SectionComment, which warns on a standalone single-line comment inside a function body that stands between two statements and introduces a further statement in the same scope, with debt markers and auto-formatter directives excluded to their own standards, detection-only (#159)
  • Clear Code: Encapsulate Related Methods in a Class — Tier 3, code review; partial slice: Action class shape sniff (#161)
  • Clear Code: Encapsulate Related Classes in a Domain — Tier 3 core, code review (#16); one Tier 2 slice, the custom sniff CleanCode.ClearCode.JunkDrawerNamespace, which warns on a namespace segment naming a generic technical bucket (Helpers, Utils, Utilities, Misc, Common, General by default, configurable), detection-only and case-insensitive, with framework layers such as Controllers and Models deliberately absent (#190)
  • Clear Code: Encapsulate Related Methods in a Class — Tier 3 core, code review: deciding that scattered logic belongs in a class of its own is a semantic judgement no single file's token stream can make (#15); the Action-class-shape slice is Tier 2, custom sniff CleanCode.ClearCode.ActionSingleEntryPoint, which warns on every public method past the first in a class whose name ends in Action or whose namespace carries an Actions segment — __construct excluded, static and implicitly-public methods included, public accessors reported like any other extra entry point, detection-only (#161)
  • Clear Code: Encapsulate Related Classes in a Domain — Tier 3, code review; partial slice: junk-drawer namespace sniff (#190)
  • Models: Structure (Attributes/Queries traits) — Tier 3, code review; partial slice carried by the custom sniff CleanCode.Models.ModelMagicMethodLocation: warns on an accessor, mutator, modern Attribute method, or scope*/#[Scope] query scope declared in a class body rather than in the model's Attributes or Queries trait, detection-only (#192)
  • Methods: Naming — Tier 3, code review; narrow command–query slice tracked in #172
  • Models: Structure (Attributes/Queries traits) — Tier 3, code review; partial slice: accessor/mutator/scope methods declared in a class body (#192)
  • Methods: Naming — Tier 3, code review; narrow command–query slice enforced by custom sniff CleanCode.Naming.ActionMethodReturn, warning severity, report-only: a method named for an action verb that declares a non-void/never return type, or returns an expression with no declared type; configurable verb list and fluent-interface exemption (#172)
  • Pattern: Don't Repeat Yourself (DRY) — Tier 2, custom sniff: repeated-block detection (#134)
  • CleanCode: BooleanArgumentFlag — Tier 2, custom sniff CleanCode.Functions.DisallowBooleanArgumentFlag, error severity, report-only; stricter than PHPMD on three shapes (#76)
  • CleanCode: DuplicatedArrayKey — Tier 2, custom sniff CleanCode.Arrays.DuplicatedArrayKey: flags an array literal that declares the same key twice, comparing the key PHP stores rather than the literal's spelling, report-only; stricter than PHPMD on four shapes, drops one PHPMD false positive, and one shape neither tool catches (#81)
  • CleanCode: ElseExpression — Tier 1, custom sniff CleanCode.Conditionals.DisallowElse: reports every else branch; Slevomat ControlStructures.EarlyExit covers only the mechanically fixable subset, so it is not used (#77). The same sniff also carries the stricter clean-code standard #14, so it reports elseif too and auto-fixes a narrow subset — both beyond the PHPMD rule
  • CleanCode: ErrorControlOperator — Tier 1, existing sniff Generic.PHP.NoSilencedErrors raised to error severity, report-only; stricter than PHPMD on one shape (#82)
  • CleanCode: IfStatementAssignment — Tier 1, existing sniff Generic.CodeAnalysis.AssignmentInCondition raised to error severity, report-only; stricter than PHPMD on the compound assignment operators, the for/while/switch/case/match conditions, and file-scope code, plus custom sniff CleanCode.Conditionals.DisallowListAssignmentInCondition for the list() destructuring target the Generic sniff misses (#79)
  • CleanCode: MissingImport — Tier 1, Slevomat Namespaces.ReferenceUsedNamesOnly, already error severity, auto-fixable; stricter than PHPMD on every reference that is not a new, and one shape neither tool catches (#84)
  • CleanCode: UndefinedVariable — Tier 1, external sniff VariableAnalysis.CodeAnalysis.VariableAnalysis raised to error severity, report-only; stricter than PHPMD on two shapes, and one shape neither tool catches (#85)
  • Controversial: Superglobals — Tier 2, custom sniff CleanCode.Controversial.Superglobals, error severity, report-only; all sixteen names PHPMD carries, including the seven PHP 4 long-form aliases, bare or interpolated into a double-quoted string or heredoc; stricter than PHPMD on file-scope access and the ${name} form, narrower on the static property access PHPMD falsely flags (#90)
  • CodeSize: CyclomaticComplexity — Tier 2, custom sniff CleanCode.Metrics.CyclomaticComplexity, error severity, report-only; replicates PDepend's ccn2 metric, which Generic.Metrics.CyclomaticComplexity cannot express at any threshold, and PHPMD's inclusive reportLevel default of 10 (#88)
  • CodeSize: ExcessiveClassComplexity — Tier 2, custom sniff CleanCode.Metrics.ExcessiveClassComplexity, error severity, report-only; the maximum property defaults to PHPMD's 50, and a class measuring exactly the maximum is reported, as PHPMD reports it (#87)
  • CodeSize: ExcessiveClassLength — Tier 2, custom sniff CleanCode.Classes.ExcessiveClassLength at PHPMD's own thresholds, report-only; one disclosed undercount where PHP_CodeSniffer mis-pairs class braces (#93)
  • CodeSize: ExcessiveMethodLength — Tier 2, custom sniff CleanCode.Functions.ExcessiveMethodLength, error severity, report-only; replicates PDepend's loc and eloc metrics and PHPMD's inclusive threshold, and covers plain functions as PHPMD does (#91)
  • CodeSize: ExcessiveParameterList — Tier 2, custom sniff CleanCode.Functions.ExcessiveParameterList, error severity, report-only; threshold minimum (default 10) is inclusive, so ten parameters violate; stricter than PHPMD on one shape (#95)
  • CodeSize: ExcessivePublicCount — Tier 2, custom sniff CleanCode.Metrics.ExcessivePublicCount, error severity, report-only; PHPMD's minimum property and its default of 45, applied inclusively (exactly 45 public members is already a violation, as in PHPMD); stricter than PHPMD on the three shapes PDepend cannot model — a trait's public properties, a constructor's promoted public properties, and an anonymous class's own members (#96)
  • CodeSize: NPathComplexity — Tier 2, custom sniff CleanCode.Metrics.NPathComplexity, error severity, report-only; the minimum property defaults to PHPMD's 200, and a callable measuring exactly the minimum is reported, as PHPMD reports it (#89)
  • CodeSize: TooManyFields — Tier 2, custom sniff CleanCode.Metrics.TooManyFields: reports a class declaring more than maxFields (default 15) fields, report-only; stricter than PHPMD on promoted properties, PHP 8.4 property modifiers, and anonymous classes (#98)
  • CodeSize: TooManyMethods — Tier 2, custom sniff CleanCode.CodeSize.TooManyMethods, report-only; PHPMD's shipped defaults (maxmethods 25, ignorepattern (^(set|get|is|has|with))i) (#80)
  • CodeSize: TooManyPublicMethods — Tier 2, custom sniff CleanCode.Classes.TooManyPublicMethods, error severity, report-only; exact parity with PHPMD on its defaults, which are taken from the shipped codesize.xml rather than phpmd.org's stale ignorepattern (#83)
  • Design: CouplingBetweenObjects — Tier 2, custom sniff CleanCode.Metrics.CouplingBetweenObjects, report-only; inclusive threshold as PHPMD implements it, stricter on the unused import #114 asks for and on scoring a nested anonymous class as its own scope, looser on the mixed/object/docblock types PDepend reads and the TypeHints rules already require (#114)
  • Design: CountInLoopExpression — Tier 2, custom sniff CleanCode.ControlStructures.DisallowCountInLoopExpression, report-only; stricter than PHPMD on two spellings it misses, looser on the first-class callable count(...) it falsely flags (#99)
  • Design: DepthOfInheritance — Tier 2, custom sniff CleanCode.Metrics.DepthOfInheritance, error severity, report-only; PHPMD's minimum property and its default of 6, applied inclusively (exactly 6 parents is already a violation, as in PHPMD); the one rule here that resolves across the whole analysed fileset, via PHP_CodeSniffer's own FileList, so phpcs src/ sees the parents phpmd src/ sees and an ancestor outside the run weighs 2 as PDepend weighs it (#112)
  • Design: DevelopmentCodeFragment — Tier 1, existing sniff CleanCode.Debug.DisallowDebugFunctions extended to PHPMD's unwanted-functions defaults, report-only (#86)
  • Design: EvalExpression — Tier 1, existing sniff Squiz.PHP.Eval raised to error severity, report-only (#107)
  • Design: ExitExpression — Tier 2, custom sniff CleanCode.ControlStructures.DisallowExitExpression, report-only; scoped to function and method bodies as PHPMD scopes it, and stricter than PHPMD on two shapes PDepend does not model (#105)
  • Design: GotoStatement — Tier 1, existing sniff Generic.PHP.DiscourageGoto raised to error severity with a replacement message, report-only; stricter than PHPMD on target labels and on goto at file scope (#109)
  • Design: NumberOfChildren — Tier 2, custom sniff CleanCode.Metrics.NumberOfChildren, report-only; the ruleset's first cross-file rule, counting direct subclasses across PHPCS's own file list, so a single-file run sees only that file's children exactly as PHPMD does; inclusive threshold as PHPMD implements it, against #110's acceptance criteria asking for the boundary to be silent (#110)
  • Naming: BooleanGetMethodName — Tier 2, custom sniff CleanCode.Naming.BooleanGetMethodName, error severity, report-only; checkParameterizedMethods matches PHPMD, and stricter than PHPMD on six shapes (#116)
  • Naming: ConstructorWithNameAsEnclosingClass — Tier 1, existing sniff Generic.NamingConventions.ConstructorName (already error severity, call-site code excluded), report-only; stricter than PHPMD on namespaced classes, looser on two shapes PHPMD misreads (#113)
  • Naming: LongClassName — Tier 2, custom sniff CleanCode.Naming.LongClassName: flags a class, interface, trait, or enum whose declared name exceeds a configurable maximum (default 40, with optional prefix/suffix subtraction), report-only (#101)
  • Naming: LongVariable — Tier 2, custom sniff CleanCode.Naming.LongVariable: flags a field, formal parameter, or local variable whose name exceeds a configurable maximum (default 20, $ not counted, with optional prefix/suffix subtraction), report-only; diverges from PHPMD on trait duplicates and string interpolation (#108)
  • Naming: ShortClassName — Tier 2, custom sniff CleanCode.Naming.ShortClassName, error severity, report-only; a class, interface, trait, or enum name shorter than minimum (default 3), with no divergence from PHPMD (#103)
  • Naming: ShortMethodName — Tier 2, custom sniff CleanCode.Naming.ShortMethodName, report-only; PHPMD's minimum and exceptions properties carried over unchanged, and stricter than PHPMD on anonymous-class methods (#111)
  • Naming: ShortVariable — Tier 2, custom sniff CleanCode.Naming.ShortVariable, report-only; PHPMD's minimum and exceptions properties carried over unchanged, stricter than PHPMD on member-access chains, catch blocks, procedural code and anonymous classes, and quieter on $this, which PHPMD reports once minimum is raised above four but no rename can fix (#106)
  • UnusedCode: UnusedFormalParameter — Tier 2, custom sniff CleanCode.DeadCode.UnusedFormalParameter, error severity, report-only; a declared parameter never read in the body, exempting an @inheritdoc or #[\Override] annotation, a same-file resolvable override, and a body-less declaration; stricter than PHPMD on an override of a parent it cannot see in the same file (#120)
  • UnusedCode: UnusedLocalVariable — Tier 1, the UnusedVariable code on the same external sniff VariableAnalysis.CodeAnalysis.VariableAnalysis, raised to error severity, report-only; three properties configured to match PHPMD, and one report per assignment where PHPMD reports one per name (#118)