PHPackages                             mglaman/phpstan-drupal - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Testing &amp; Quality](/categories/testing)
4. /
5. mglaman/phpstan-drupal

ActivePhpstan-extension[Testing &amp; Quality](/categories/testing)

mglaman/phpstan-drupal
======================

Drupal extension and rules for PHPStan

2.1.1(1mo ago)20832.2M↓28.1%86[88 issues](https://github.com/mglaman/phpstan-drupal/issues)[18 PRs](https://github.com/mglaman/phpstan-drupal/pulls)20MITPHPPHP ^8.1CI passing

Since Jan 9Pushed 1w ago5 watchersCompare

[ Source](https://github.com/mglaman/phpstan-drupal)[ Packagist](https://packagist.org/packages/mglaman/phpstan-drupal)[ GitHub Sponsors](https://github.com/mglaman)[ Fund](https://opencollective.com/phpstan-drupal)[ RSS](/packages/mglaman-phpstan-drupal/feed)WikiDiscussions main Synced 2w ago

READMEChangelog (10)Dependencies (60)Versions (167)Used By (20)

phpstan-drupal
==============

[](#phpstan-drupal)

[![Tests](https://github.com/mglaman/phpstan-drupal/actions/workflows/php.yml/badge.svg)](https://github.com/mglaman/phpstan-drupal/actions/workflows/php.yml) [![CircleCI](https://camo.githubusercontent.com/fdc911b98f15d9423a41a545bf6231fc9a82155d26672462f4e0c74acb3b143f/68747470733a2f2f636972636c6563692e636f6d2f67682f6d676c616d616e2f7068707374616e2d64727570616c2e7376673f7374796c653d737667)](https://circleci.com/gh/mglaman/phpstan-drupal)

Extension for [PHPStan](https://phpstan.org/) to allow analysis of Drupal code.

PHPStan is able to [discover symbols](https://phpstan.org/user-guide/discovering-symbols) by using autoloading provided by Composer. However, Drupal does not provide autoloading information for modules and themes. This project registers those namespaces so that PHPStan can properly discover symbols in your Drupal code base automatically.

Note

With Drupal 11.2, Drupal core is now using PHPStan 2.0. The 1.x branch of phpstan-drupal will be supported until Drupal 10 loses security support when Drupal 12 is released.

Sponsors
--------

[](#sponsors)

[![Fame Helsinki](https://camo.githubusercontent.com/cf0352bec0a349eef4e20f93496e3af5a2da9f7500b34ed40d48ce0fd7a8bd4a/68747470733a2f2f7777772e66616d652e66692f6173736574732f696d616765732f66616d652d6c6f676f2e706e67)](https://www.fame.fi/)

[Would you like to sponsor?](https://github.com/sponsors/mglaman)

Usage
-----

[](#usage)

When you are using [`phpstan/extension-installer`](https://github.com/phpstan/extension-installer), `phpstan.neon` will be automatically included.

 Manual installationIf you don't want to use `phpstan/extension-installer`, include `extension.neon` in your project's PHPStan config:

```
includes:
    - vendor/mglaman/phpstan-drupal/extension.neon

```

To include Drupal specific analysis rules, include this file:

```
includes:
    - vendor/mglaman/phpstan-drupal/rules.neon

```

Getting help
------------

[](#getting-help)

Ask for assistance in the [discussions](https://github.com/mglaman/phpstan-drupal/discussions) or [\#phpstan](https://drupal.slack.com/archives/C033S2JUMLJ) channel on Drupal Slack.

Excluding tests from analysis
-----------------------------

[](#excluding-tests-from-analysis)

To exclude tests from analysis, add the following parameter

```
parameters:
	excludePaths:
		- *Test.php
		- *TestBase.php

```

Deprecation testing
-------------------

[](#deprecation-testing)

This project depends on `phpstan/phpstan-deprecation-rules` which adds deprecation rules. We provide Drupal-specific deprecated scope resolvers.

To only handle deprecation testing, use a `phpstan.neon` like this:

```
parameters:
	customRulesetUsed: true
	reportUnmatchedIgnoredErrors: false
	# Ignore phpstan-drupal extension's rules.
	ignoreErrors:
		- '#\Drupal calls should be avoided in classes, use dependency injection instead#'
		- '#Plugin definitions cannot be altered.#'
		- '#Missing cache backend declaration for performance.#'
		- '#Plugin manager has cache backend specified but does not declare cache tags.#'
includes:
	- vendor/mglaman/phpstan-drupal/extension.neon
	- vendor/phpstan/phpstan-deprecation-rules/rules.neon

```

To disable deprecation rules while using `phpstan/extension-installer`, you can do the following:

```
{
  "extra": {
    "phpstan/extension-installer": {
      "ignore": [
        "phpstan/phpstan-deprecation-rules"
      ]
    }
  }
}
```

See the `extension-installer` documentation for more information:

Adapting to your project
------------------------

[](#adapting-to-your-project)

### Customizing rules

[](#customizing-rules)

#### Default rules promoted in 2.1.0

[](#default-rules-promoted-in-210)

As of 2.1.0, the following rules are enabled by default. Disable any of them individually if they don't fit your project:

```
parameters:
    drupal:
        rules:
            # Enforces that OOP hook implementations using the Hook attribute have the
            # correct method signature for hook_form_alter, hook_form_FORM_ID_alter, etc.
            # Requires Drupal 10.3+ (Hook attribute).
            hookFormAlterRule: false

            # Flags non-abstract test classes whose names do not end with "Test".
            testClassSuffixNameRule: false

            # Flags properties that are private or read-only in classes using
            # DependencySerializationTrait, which does not support them.
            dependencySerializationTraitPropertyRule: false

            # Flags calls to AccessResult static methods (::allowed(), ::forbidden(), etc.)
            # whose argument type already makes the condition always true or always false.
            accessResultConditionRule: false

            # Flags addCacheableDependency() calls whose argument does not implement
            # CacheableDependencyInterface.
            cacheableDependencyRule: false

            # Flags logger channel objects (from LoggerChannelFactoryInterface::get()) that
            # are assigned to a property in a class using DependencySerializationTrait,
            # which cannot serialize logger channels correctly.
            loggerFromFactoryPropertyAssignmentRule: false

            # Flags direct injection of EntityStorageInterface (or a subtype) into a
            # constructor. Inject EntityTypeManagerInterface and call getStorage() instead.
            entityStorageDirectInjectionRule: false

            # Flags direct use of Symfony\Component\Yaml\Yaml::parse() on Drupal-controlled
            # YAML files, which should go through Drupal's YAML parser.
            symfonyYamlParseRule: false

            # Flags cacheability issues in entity list builder and entity operation hooks.
            entityOperationsCacheabilityRule: false
```

Note

`hookRules` was renamed to `hookFormAlterRule` in 2.1.0 — update your configuration if you referenced it explicitly.

#### Disabling checks for extending `@internal` classes

[](#disabling-checks-for-extending-internal-classes)

You can disable the `ClassExtendsInternalClassRule` rule by adding the following to your `phpstan.neon`:

```
parameters:
    drupal:
        rules:
            classExtendsInternalClassRule: false
```

#### Disabling extensions

[](#disabling--extensions)

You can disable various extensions. This is useful when contributing to Drupal Core to improve its types.

```
parameters:
    drupal:
        extensions:
            entityFieldsViaMagicReflection: true
            entityFieldMethodsViaMagicReflection: true
            entityQuery: true
            entityRepository: true
            stubFiles: true
```

Both options are enabled by default.

#### Class resolver return types

[](#class-resolver-return-types)

`ClassResolverInterface::getInstanceFromDefinition()` and `\Drupal::classResolver()` calls are narrowed to the type of the passed class name or service ID. The return value is not guaranteed at runtime: the container is checked first, and a service definition can substitute a different class. Disable the narrowing to keep `instanceof` assertions meaningful:

```
parameters:
    drupal:
        classResolverReturnType: false
```

Enabled by default. When disabled, calls fall back to the declared `object` return type.

#### Bleeding-edge checks

[](#bleeding-edge-checks)

`bleedingEdge.neon` enables hook deprecation checks against `.api.php` files and stricter service-container checking. New rules land here first before graduating to the default ruleset in a minor release.

```
includes:
    - vendor/mglaman/phpstan-drupal/bleedingEdge.neon
```

What it currently enables:

- `checkCoreDeprecatedHooksInApiFiles` — reports hook implementations deprecated in Drupal core `.api.php` files
- `checkContribDeprecatedHooksInApiFiles` — reports hook implementations deprecated in contrib module `.api.php` files

Note

`checkDeprecatedHooksInApiFiles` is deprecated. Use `checkCoreDeprecatedHooksInApiFiles` and `checkContribDeprecatedHooksInApiFiles` instead.

Note

`containerHasAlwaysTrue: false` graduated from bleeding edge to the default in 2.1.0. `ContainerInterface::has()` returns `bool` instead of always-`true` for known services, so conditional service guards stay meaningful. Restore the old inference with:

```
parameters:
    drupal:
        bleedingEdge:
            containerHasAlwaysTrue: true
```

#### Config schema-based checks (experimental)

[](#config-schema-based-checks-experimental)

Two opt-in features use Drupal's config schema files to analyze `Config::get()` calls. Both only act on config objects whose schema has the `FullyValidatable` constraint, since only those schemas are guaranteed complete. Schema files are parsed lazily on first use, so there is no cost when the features are disabled.

```
parameters:
    drupal:
        # Narrows Config::get() return types from the config schema, e.g.
        # \Drupal::config('system.cron')->get('logging') resolves to bool|null
        # instead of mixed. Types are nullable because any key can be absent
        # at runtime.
        configGetReturnType: true
        rules:
            # Reports Config::get() calls with a key that does not exist in the
            # config schema, catching typos at analysis time.
            configGetUnknownKeyRule: true
```

Recognized call patterns:

- `\Drupal::config('...')->get('...')`
- `$configFactory->get('...')->get('...')`
- `$configFactory->getEditable('...')->get('...')`
- `$this->config('...')->get('...')` in classes using `ConfigFormBaseTrait`

The config name and key must be literal strings. Config entity schemas with wildcard names (e.g. `block.block.*`) are not supported yet, and keys beneath dynamic type references (e.g. `mailer_dsn.options.[%parent.scheme]`) are not validated.

Neither feature is included in `bleedingEdge.neon` yet.

#### Detecting @todo comments referencing the current Drupal.org issue (contrib CI)

[](#detecting-todo-comments-referencing-the-current-drupalorg-issue-contrib-ci)

`TodoCommentWithIssueUrlRule` is an opt-in rule for Drupal contrib CI pipelines. When running PHPStan as part of a GitLab merge request, it reports an error for any `@todo` comment that contains a drupal.org issue URL matching the current issue — for example:

```
// @todo Remove once https://drupal.org/i/3456789 is resolved.
```

This prevents issue-specific TODOs from being accidentally merged without resolution.

The rule auto-detects the current issue NID from standard GitLab CI environment variables:

- `CI_MERGE_REQUEST_SOURCE_BRANCH_NAME` (e.g. `3456789-my-feature`)
- `CI_MERGE_REQUEST_SOURCE_PROJECT_PATH` (e.g. `issue/mymodule-3456789`)

It is silent when neither variable is set, so it is safe to include in a shared config.

The rule is **not registered by default**. To enable it, add it to your project's `phpstan.neon`:

```
rules:
    - mglaman\PHPStanDrupal\Rules\Drupal\TodoCommentWithIssueUrlRule
```

Note

When using the [Drupal GitLab CI templates](https://project.pages.drupalcode.org/gitlab_templates/jobs/phpstan/), adding extra rules requires a custom `phpstan.neon` that includes the default configuration, since adding additional rules is not supported directly through the template variables.

Both `drupal.org/i/{nid}` and `drupal.org/project/{project}/issues/{nid}` URL formats are recognized.

### Custom PHPDoc types

[](#custom-phpdoc-types)

phpstan-drupal provides custom PHPDoc types that can be used to improve type safety in Drupal code.

#### `entity-type-id`

[](#entity-type-id)

The `entity-type-id` type represents a valid Drupal entity type ID string (e.g. `'node'`, `'user'`, `'taxonomy_term'`). PHPStan will report an error when a constant string that is not a known entity type ID is passed where `entity-type-id` is expected.

Drupal coding standards require keeping PHPStan-specific types in `@phpstan-param` and `@phpstan-return` tags rather than in the standard `@param` and `@return` tags:

```
/**
 * Loads an entity by its entity type ID and entity ID.
 *
 * @param string $entityTypeId
 *   The entity type ID.
 * @param int|string $id
 *   The entity ID.
 *
 * @phpstan-param entity-type-id $entityTypeId
 */
public function loadEntity(string $entityTypeId, int|string $id): ?EntityInterface {
    return $this->entityTypeManager->getStorage($entityTypeId)->load($id);
}
```

For return types:

```
/**
 * Returns the entity type ID.
 *
 * @return string
 *   The entity type ID.
 *
 * @phpstan-return entity-type-id
 */
public function getEntityTypeId(): string {
    return $this->entityTypeId;
}
```

Known entity type IDs are sourced from the `drupal.entityMapping` parameter. See [Entity storage mappings](#entity-storage-mappings) for how to register custom entity types so their IDs are also recognized.

### Entity storage mappings.

[](#entity-storage-mappings)

The `EntityTypeManagerGetStorageDynamicReturnTypeExtension` service helps map dynamic return types. This inspects the passed entity type ID and tries to return a known storage class, besides the default `EntityStorageInterface`. The default mapping can be found in `extension.neon`. For example:

```
parameters:
	drupal:
		entityMapping:
			block:
				class: Drupal\block\Entity\Block
				storage: Drupal\Core\Config\Entity\ConfigEntityStorage
			node:
				class: Drupal\node\Entity\Node
				storage: Drupal\node\NodeStorage
			taxonomy_term:
				class: Drupal\taxonomy\Entity\Term
				storage: Drupal\taxonomy\TermStorage
			user:
				class: Drupal\user\Entity\User
				storage: Drupal\user\UserStorage

```

To add support for custom entities, you may add the same definition in your project's `phpstan.neon`. See the following example for adding a mapping for Search API:

```
parameters:
	drupal:
		entityMapping:
			search_api_index:
				class: Drupal\search_api\Entity\Index
				storage: Drupal\search_api\Entity\SearchApiConfigEntityStorage
			search_api_server:
				class: Drupal\search_api\Entity\Server
				storage: Drupal\search_api\Entity\SearchApiConfigEntityStorage

```

Similarly, the `EntityStorageDynamicReturnTypeExtension` service helps to determine the type of the entity which is loaded, created etc.. when using an entity storage. For instance when using

```
$node = \Drupal::entityTypeManager()->getStorage('node')->create(['type' => 'page', 'title' => 'foo']);
```

It helps with knowing the type of the `$node` variable is `Drupal\node\Entity\Node`.

The default mapping can be found in `extension.neon`:

```
parameters:
	drupal:
		entityMapping:
			block:
				class: Drupal\block\Entity\Block
				storage: Drupal\Core\Config\Entity\ConfigEntityStorage
			node:
				class: Drupal\node\Entity\Node
				storage: Drupal\node\NodeStorage
			taxonomy_term:
				class: Drupal\taxonomy\Entity\Term
				storage: Drupal\taxonomy\TermStorage
			user:
				class: Drupal\user\Entity\User
				storage: Drupal\user\UserStorage
```

To add support for custom entities, you may add the same definition in your project's `phpstan.neon` likewise.

### Providing entity type mappings for a contrib module

[](#providing-entity-type-mappings-for-a-contrib-module)

Contributed modules can provide their own mapping that can be automatically registered with a user's code base when they use the `phpstan/extension-installer`. The extension installer scans installed package's `composer.json` for a value in `extra.phpstan`. This will automatically bundle the defined include that contains an entity mapping configuration.

For example, the Paragraphs module could have the following `entity_mapping.neon` file:

```
parameters:
	drupal:
		entityMapping:
			paragraph:
				class: Drupal\paragraphs\Entity\Paragraph
			paragraphs_type:
				class: Drupal\paragraphs\Entity\ParagraphsType
```

Then in the `composer.json` for Paragraphs, the `entity_mapping.neon` would be provided as a PHPStan include

```
{
  "name": "drupal/paragraphs",
  "description": "Enables the creation of Paragraphs entities.",
  "type": "drupal-module",
  "license": "GPL-2.0-or-later",
  "require": {
    "drupal/entity_reference_revisions": "~1.3"
  },
  "extra": {
    "phpstan": {
      "includes": [
        "entity_mapping.neon"
      ]
    }
  }
}
```

###  Health Score

76

—

ExcellentBetter than 100% of packages

Maintenance87

Actively maintained with recent releases

Popularity68

Solid adoption and visibility

Community49

Growing community involvement

Maturity89

Battle-tested with a long release history

 Bus Factor1

Top contributor holds 77.6% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~22 days

Recently: every ~1 days

Total

121

Last Release

31d ago

Major Versions

1.3.4 → 2.0.32025-04-03

1.3.5 → 2.0.42025-04-10

1.3.7 → 2.0.52025-04-15

1.3.8 → 2.0.62025-05-20

1.x-dev → 2.0.72025-05-22

PHP version history (4 changes)0.10.2PHP ^7.1

0.12.9PHP ^7.1 || ^8.0

1.1.5PHP ^7.4 || ^8.0

1.2.12PHP ^8.1

### Community

Maintainers

![](https://www.gravatar.com/avatar/98c03d4d2a6aa9b5ecdfc565e8f90f53f0a8f6c2757048a148b34b166d78266c?d=identicon)[mglaman](/maintainers/mglaman)

---

Top Contributors

[![mglaman](https://avatars.githubusercontent.com/u/3698644?v=4)](https://github.com/mglaman "mglaman (626 commits)")[![Boegie](https://avatars.githubusercontent.com/u/18569707?v=4)](https://github.com/Boegie "Boegie (27 commits)")[![brambaud](https://avatars.githubusercontent.com/u/9115419?v=4)](https://github.com/brambaud "brambaud (18 commits)")[![dependabot[bot]](https://avatars.githubusercontent.com/in/29110?v=4)](https://github.com/dependabot[bot] "dependabot[bot] (14 commits)")[![dpi](https://avatars.githubusercontent.com/u/21850?v=4)](https://github.com/dpi "dpi (14 commits)")[![Niklan](https://avatars.githubusercontent.com/u/2356744?v=4)](https://github.com/Niklan "Niklan (13 commits)")[![eiriksm](https://avatars.githubusercontent.com/u/865153?v=4)](https://github.com/eiriksm "eiriksm (12 commits)")[![jacktonkin](https://avatars.githubusercontent.com/u/1344557?v=4)](https://github.com/jacktonkin "jacktonkin (10 commits)")[![alexpott](https://avatars.githubusercontent.com/u/769634?v=4)](https://github.com/alexpott "alexpott (7 commits)")[![goba](https://avatars.githubusercontent.com/u/235185?v=4)](https://github.com/goba "goba (6 commits)")[![mad-briller](https://avatars.githubusercontent.com/u/28307684?v=4)](https://github.com/mad-briller "mad-briller (5 commits)")[![webflo](https://avatars.githubusercontent.com/u/123946?v=4)](https://github.com/webflo "webflo (4 commits)")[![ceesgeene](https://avatars.githubusercontent.com/u/10079586?v=4)](https://github.com/ceesgeene "ceesgeene (4 commits)")[![longwave](https://avatars.githubusercontent.com/u/197817?v=4)](https://github.com/longwave "longwave (4 commits)")[![drupol](https://avatars.githubusercontent.com/u/252042?v=4)](https://github.com/drupol "drupol (3 commits)")[![cmlara](https://avatars.githubusercontent.com/u/2461270?v=4)](https://github.com/cmlara "cmlara (3 commits)")[![jibran](https://avatars.githubusercontent.com/u/2111106?v=4)](https://github.com/jibran "jibran (3 commits)")[![mallezie](https://avatars.githubusercontent.com/u/1926222?v=4)](https://github.com/mallezie "mallezie (3 commits)")[![mondrake](https://avatars.githubusercontent.com/u/1174864?v=4)](https://github.com/mondrake "mondrake (3 commits)")[![ondrejmirtes](https://avatars.githubusercontent.com/u/104888?v=4)](https://github.com/ondrejmirtes "ondrejmirtes (3 commits)")

---

Tags

drupalhacktoberfestphpphpstanstatic-analysis

###  Code Quality

TestsPHPUnit

Code StylePHP\_CodeSniffer

Type Coverage Yes

### Embed Badge

![Health badge](/badges/mglaman-phpstan-drupal/health.svg)

```
[![Health](https://phpackages.com/badges/mglaman-phpstan-drupal/health.svg)](https://phpackages.com/packages/mglaman-phpstan-drupal)
```

###  Alternatives

[phpro/grumphp

A composer plugin that enables source code quality checks.

4.3k17.0M1.1k](/packages/phpro-grumphp)[deptrac/deptrac

Deptrac is a static code analysis tool that helps to enforce rules for dependencies between software layers.

3.0k9.8M275](/packages/deptrac-deptrac)[friendsoftypo3/content-blocks

TYPO3 CMS Content Blocks - Content Types API | Define reusable components via YAML

103574.3k69](/packages/friendsoftypo3-content-blocks)[drupal/core

Drupal is an open source content management platform powering millions of websites and applications.

19467.3M1.9k](/packages/drupal-core)[drupal/core-recommended

Locked core dependencies; require this project INSTEAD OF drupal/core.

6943.5M449](/packages/drupal-core-recommended)[wp-cli/wp-cli-tests

WP-CLI testing framework

423.3M150](/packages/wp-cli-wp-cli-tests)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
