Code Quality And Tooling

Mago PHP Toolchain Orientation

This makes Mago useful to evaluate when a project wants a consolidated quality workflow. It does not mean an established PHPStan, Psalm, PHP-CS-Fixer, PHP_CodeSniffer, or architecture-checking setup should be replaced automatically. Tool migrations need a measurable benefit and a reviewable rollout.

Install And Initialise Mago

Mago can be installed as a standalone binary, through an official container image, or as a Composer development dependency. For a Composer-managed project, follow the current official version guidance:

composer require --dev carthage-software/mago
vendor/bin/mago init

The Composer package is a wrapper that downloads and caches the matching pre-built Mago binary on first use. Teams should account for that first download in restricted development and CI environments.

mago init inspects the project and creates mago.toml. Commit that file so every developer and CI job uses the same source paths, target PHP version, integrations, and tool settings.

The Four Tools

Formatter

The formatter produces deterministic PHP layout and supports presets including PER-CS, PSR-12, Laravel, and Drupal styles.

vendor/bin/mago format
vendor/bin/mago format --check
vendor/bin/mago format --dry-run

Use format when you intend to rewrite files. Use --check in CI, where the job should report drift without modifying the checkout. Use --dry-run to inspect the proposed diff.

The distinction matters in automation: --check exits with status 1 when a file would change, while --dry-run prints a diff and exits successfully. A CI formatting gate therefore needs --check, not merely --dry-run.

Linter

The linter checks correctness, consistency, clarity, redundancy, safety, and security rules. It also supports framework integrations.

vendor/bin/mago lint

Many findings can be fixed automatically, but automatic fixes still need a diff review and the project's normal tests.

Analyser

The analyser performs whole-project type and flow analysis.

vendor/bin/mago analyze

It understands PHPStan and Psalm annotations, including generics and conditional types. Compatibility with annotations does not guarantee identical findings or suppression behaviour, so compare results before replacing another analyser.

Architectural Guard

The guard enforces dependency boundaries and structural rules.

vendor/bin/mago guard

A project can prevent a domain namespace from importing infrastructure code, restrict dependencies between modules, or require controller classes to follow naming and modifier conventions. These rules codify architecture; they do not decide the right architecture for the team.

Configure Project Boundaries

A generated mago.toml records the project paths and tool settings. Important concepts include:

  • source paths that Mago may check and format
  • include paths, such as dependencies read for context
  • excludes for generated files, caches, and other ignored content
  • the target PHP version
  • formatter presets and linter integrations
  • a Mago version pin, which may constrain the major, minor, or exact version

Review generated configuration before committing it. A formatter pointed at generated or vendor code can create a large unwanted diff, while an analyser missing application paths can give false confidence.

mago init currently writes a major-version pin by default. A major pin prevents an incompatible major release from silently reading the same configuration, while an exact pin gives a team stricter reproducibility at the cost of more deliberate upgrades.

Build A Repeatable Workflow

Prove What Each Tool Will Read

Before the first formatting or analysis run, inspect the effective scope:

vendor/bin/mago list-files --command formatter
vendor/bin/mago list-files --command linter
vendor/bin/mago list-files --command analyzer
vendor/bin/mago config --show source

The lists can differ because global source exclusions and tool-specific exclusions are additive. Dependencies in source.includes are parsed for symbols and types but are not treated as project files to rewrite or report as first-party code. Generated files, caches, and build output normally belong in exclusions; application code should not be excluded merely because it currently has findings.

Also record the actual binary version:

vendor/bin/mago --version

The Composer wrapper downloads a platform binary, so a committed lock file and mago.toml version pin are both relevant to reproducibility. CI should fail visibly if it cannot obtain the pinned binary rather than silently skipping the check.

Introduce Existing Findings Honestly

A mature project may produce many findings on its first run. Classify them before changing the quality gate:

  • defects that should be fixed immediately;
  • valid findings that can be repaired incrementally;
  • configuration gaps, such as missing source or include paths;
  • false positives that need a narrow, explained suppression;
  • rules the team has deliberately decided not to enforce.

Use Mago's baseline or suppression facilities only to establish an explicit migration boundary. A baseline should represent known existing debt while new or changed code remains accountable. Recreate or shrink it as findings are fixed, and review inline suppressions like code because they can hide future defects at the same location.

Excluding an entire legacy directory is usually weaker than recording specific known findings: the exclusion prevents all four tools from seeing later problems in that code.

A project might expose one Composer script per check:

{
  "scripts": {
    "mago:format": "mago format --check",
    "mago:lint": "mago lint",
    "mago:analyze": "mago analyze",
    "mago:guard": "mago guard"
  }
}

Keep local and CI commands aligned. Fast checks can run on every pull request, while a newly introduced architectural rule may need a staged rollout before it becomes blocking.

Evaluate Adoption Deliberately

Before adopting Mago, answer these questions:

  • Which current problem does it solve?
  • Which existing tools would remain or be retired?
  • Do the formatter and analyser agree with current project contracts?
  • Can existing annotations, suppressions, baselines, and CI reporting be preserved?
  • Will developers and CI obtain the same pinned binary?
  • Can the migration be trialled on a representative directory first?

Do not run two formatters that disagree. Multiple analysers can be useful during a trial, but duplicated findings and different type models may create more noise than value.

What To Remember

Mago combines formatting, linting, static analysis, and architecture enforcement in one binary configured through mago.toml. Use the specific subcommand that matches the risk, keep check mode non-mutating in CI, pin shared configuration, and replace established tools only after a focused comparison.

Before moving on, make sure you can choose between format --check, lint, analyze, and guard, and explain why annotation compatibility does not make analyser migrations automatic.

Practice

Task: Design A Small Mago Trial

A project currently uses PHP-CS-Fixer and PHPStan. The team wants to evaluate Mago without disrupting every pull request. The first Mago run also reports existing issues in legacy code.

Create a trial plan that:

  • selects one representative source directory;
  • records the installed Mago version and committed version pin;
  • proves which files the formatter, linter, and analyser will inspect;
  • runs Mago formatting without rewriting files;
  • runs linting and analysis separately;
  • leaves the existing PHP-CS-Fixer and PHPStan checks in place during the trial;
  • records differences in formatting and analyser findings;
  • classifies existing findings before creating any baseline or suppression;
  • avoids excluding all legacy code merely to get a green result;
  • avoids enabling the architectural guard until the team defines a real boundary;
  • states separate success criteria for adopting or rejecting each Mago tool.

Include the Mago commands you would run. Explain why replacing both existing tools immediately, or making the first Mago run blocking, would make the evaluation harder to trust.

Show solution
vendor/bin/mago --version
vendor/bin/mago config --show source
vendor/bin/mago list-files --command formatter
vendor/bin/mago list-files --command linter
vendor/bin/mago list-files --command analyzer

Confirm that src/Orders is first-party source, dependencies are includes rather than rewrite targets, and generated files are excluded deliberately. Then run each comparison separately:

vendor/bin/mago format --dry-run src/Orders
vendor/bin/mago lint src/Orders
vendor/bin/mago analyze src/Orders

format --dry-run prints the proposed diff without rewriting files. CI would later use format --check, because check mode returns a failing status when formatting differs.

Review the formatter diff against PHP-CS-Fixer. Classify Mago findings as real defects, useful stricter checks, existing debt, false positives, or configuration gaps. If the volume requires a baseline, create it only after that review and keep it as a visible boundary for existing debt. Use narrow explained suppressions for genuine false positives instead of excluding the whole legacy area.

Record each component independently:

Component Evidence to collect Adoption threshold
Formatter Diff size, disputed rules, runtime, and interaction with generated files It can replace PHP-CS-Fixer only when the team accepts the resulting style and one formatter can become authoritative
Linter Unique defects, noisy rules, safe-fix quality, suppression cost, and runtime Its useful findings outweigh configuration and maintenance cost
Analyser Findings compared with PHPStan, annotation compatibility, false positives, baselined debt, and missed defects It preserves required PHPStan coverage and produces an acceptable signal-to-noise ratio
Guard No trial yet A named dependency or structural boundary has an owner, examples, and an agreed failure policy

Keep PHP-CS-Fixer and PHPStan blocking throughout the trial. Run Mago as a non-blocking comparison job first, publish its output, and review several representative changes rather than one clean snapshot.

Replacing both tools immediately changes formatting and type-analysis behaviour in the same pull request, making regressions difficult to attribute. Making the first run blocking turns unclassified historical findings into an emergency and encourages broad exclusions. Adopt or reject the formatter, linter, analyser, and guard separately, then remove an incumbent tool only after equivalent coverage and developer workflow have been demonstrated.