webgriffe/psalm-plugin-magento2

Psalm plugin that stops UnusedClass/UnusedMethod false positives on Magento 2 framework-instantiated classes (Controllers, Blocks, Observers, Cron jobs, Plugins, Setup Patches, Console Commands)

Maintainers

Package info

github.com/webgriffe/psalm-plugin-magento2

Type:psalm-plugin

pkg:composer/webgriffe/psalm-plugin-magento2

Transparency log

Statistics

Installs: 9

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.0.1 2026-08-26 14:45 UTC

This package is auto-updated.

Last update: 2026-08-26 14:46:07 UTC


README

Stops Psalm's UnusedClass / UnusedMethod / PossiblyUnusedMethod checks from false-positiving on Magento 2 classes that the framework only ever instantiates reflectively — by class name in routes.xml, layout XML, crontab.xml or di.xml — never via a direct new X() or a type-hint Psalm's static analysis can trace back to a caller.

With Psalm's findUnusedCode on by default (Psalm ≥5), a class like this:

class Index extends \Magento\Customer\Controller\AbstractAccount
{
    public function execute()
    {
        return $this->resultFactory->create(ResultFactory::TYPE_PAGE);
    }
}

gets flagged UnusedClass: Class ... is never used even though Magento's front controller invokes it on every request to its route — Psalm just has no way to see routes.xml wiring a URL to this class name.

What it covers

Matched by contract, wherever Magento provides one (interface or common base class) — works regardless of which module or folder the class lives in:

Category Matched by
Controllers extends/implements Magento\Framework\App\ActionInterface, Action, AbstractAction, or \Magento\Customer\Controller\AbstractAccount
Blocks extends Magento\Framework\View\Element\AbstractBlock or Template
Observers implements Magento\Framework\Event\ObserverInterface
Setup patches implements DataPatchInterface / SchemaPatchInterface
Console commands extends Symfony\Component\Console\Command\Command

Cron jobs and Plugins (interceptors) have no common Magento interface/base classcrontab.xml/di.xml reference a bare class name + method by convention only. Those two fall back to a namespace segment check (FQCN contains \Cron\ or \Plugin\) — the one convention-based (not contract-based) rule here.

Known limitation

Only directly declared extends/parent class and directly implemented interfaces are inspected, not multi-level-inherited ones. A project-specific AbstractController implements ActionInterface, with real controllers extending that instead of a Magento class directly, won't be picked up. Open an issue/PR if this bites you.

Install

composer require --dev webgriffe/psalm-plugin-magento2

Then wire it into psalm.xml:

<plugins>
    <pluginClass class="Webgriffe\PsalmPluginMagento2\PluginEntryPoint" />
</plugins>

Why not just findUnusedCode="false"?

That silences dead-code detection for the entire codebase — including genuinely dead Helper/Model/Service/Repository classes, which is exactly the kind of thing Psalm's unused-code detection is useful for. This plugin only exempts the specific categories Magento wires outside PHP's own reference graph, so the rest of the codebase keeps real dead-code detection.

Compatibility note for phar-based Psalm distributions

Some Psalm distributions (e.g. tools that bundle Psalm as a single phar with its dependencies scoped under a private namespace prefix, to avoid colliding with the host project's own Composer dependencies) rewrite nikic/php-parser's classes under a prefix like PsalmPhar\PhpParser\.... An instanceof \PhpParser\Node\Stmt\Class_ check silently never matches in that case, even though the visited node's shape is identical. This plugin matches on the resolved Psalm\Storage\ClassLikeStorage data (parent_class, class_implements) and a class-name suffix check instead, which works under both plain Composer-installed Psalm and phar-scoped distributions.

License

MIT