blockstudio / phpstan
PHPStan extension for Blockstudio with template typing, schema validation, block tag checks, public API stubs, and a generated project contract.
Package info
github.com/inline0/blockstudio-phpstan
Type:phpstan-extension
pkg:composer/blockstudio/phpstan
Requires
- php: ^8.2
- nikic/php-parser: ^5.0
- phpstan/phpstan: ^2.0
- szepeviktor/phpstan-wordpress: ^2.0
Requires (Dev)
- phpunit/phpunit: ^10.5
README
PHPStan extension for Blockstudio. It adds type-safe template access, schema validation, hook checking, and stubs for the Blockstudio public PHP API.
Install
composer require --dev blockstudio/phpstan
If you have
phpstan/extension-installer
installed, the extension is auto-discovered. Otherwise, include it manually in
your phpstan.neon:
includes: - vendor/blockstudio/phpstan/extension.neon
What it checks
Template field access
When a PHP file lives next to a block.json, the extension validates every
$a['key'] access against the block's declared attributes.
// blockstudio/hero/index.php <?php echo $a['title']; // OK echo $a['subtitle']; // OK echo $a['typo']; // Error: Field "typo" does not exist in block.json
Field keys follow Blockstudio's runtime flattening rules. tabs and anonymous
group containers without an id keep their children in the parent scope.
Named groups prefix child keys, so a text field inside a cta group is
available as $a['cta_text'].
File-backed reusable fields are expanded too. A
{"type":"custom/mytheme/hero","idStructure":"hero_{id}"} reference contributes
the same transformed keys as it does at runtime, including nested custom
references inside groups, tabs, and repeaters. Per-instance overrides are
applied before PHP, Twig, Blade, block-tag, and inferred-shape checks.
Missing, ambiguous, invalid, and cyclic definitions report focused
blockstudio.customField.* errors without producing secondary unknown-key
noise. Definitions registered only through the runtime blockstudio/fields
filter cannot be inferred; use field.json for statically checked fields.
Twig and Blade templates are checked too:
<h1>{{ a.title }}</h1> <p>{{ a.typo }}</p> {# Error #}
<h1>{{ $a['title'] }}</h1> <p>{{ $a['typo'] }}</p> {{-- Error --}}
Block tag validation
Both <block> and <bs:> tag syntaxes are validated across PHP, Twig, and
Blade templates.
<bs:mytheme-hero title="Hello" /> <!-- OK --> <bs:mytheme-nonexistent /> <!-- Error: unknown block --> <bs:mytheme-hero badattr="" /> <!-- Error: unknown attribute --> <block name="core/separator" /> <!-- OK -->
data-* and html-* attributes are treated as pass-through and are not
validated.
Typed Db::get() records
The extension reads your db.php schema and uses it to type record arrays
returned by Db::get().
// blockstudio/subscribers/db.php return [ 'storage' => 'table', 'fields' => [ 'email' => ['type' => 'string', 'required' => true], 'name' => ['type' => 'string'], ], ]; $db = Db::get('mytheme/subscribers'); $record = $db->create(['email' => 'a@b.com']); echo $record['email']; // string echo $record['name']; // string|null echo $record['typo']; // Error
This also works with the PHP-native builder syntax:
use Blockstudio\Db\Field; use Blockstudio\Db\Schema; use Blockstudio\Db\Storage; return Schema::make( storage: Storage::Table, fields: [ 'email' => Field::string(required: true), 'active' => Field::boolean(default: false), ], );
Settings path validation
Settings::get() paths are checked against the known Blockstudio settings
schema.
Settings::get('tailwind/enabled'); // OK Settings::get('tailwind/enabld'); // Error: Did you mean "tailwind/enabled"?
Hook name validation
Blockstudio action and filter hook names are validated.
add_filter('blockstudio/render', $cb); // OK add_filter('blockstudio/rendrr', $cb); // Error
Dynamic settings hooks such as blockstudio/settings/tailwind/enabled are
always allowed. Non-Blockstudio hooks are ignored.
Schema validation
The extension validates Blockstudio schema files across the project:
block.jsonfield.json- extension JSON files in
extensions/ page.jsondb.phprpc.phpcron.phpblockstudio.json
That covers missing required keys, invalid field types, malformed schema
shapes, bad RPC method values, invalid cron schedules, and deprecated settings
shorthand such as "tailwind": true.
db.php, rpc.php, and cron.php support both legacy arrays and the optional
PHP-native forms:
Blockstudio\Db\Schema/Blockstudio\Db\Field#[Blockstudio\Attributes\Rpc]#[Blockstudio\Attributes\Cron]
API stubs
The package ships stubs for the Blockstudio public API, including:
Db,Settings,Build,Field_RegistryBlockstudio\Db\Schema,Blockstudio\Db\Field,Blockstudio\Db\StorageBlockstudio\Rpc\Method,Blockstudio\Rpc\AccessBlockstudio\Cron\ScheduleBlockstudio\Attributes\Rpc,Blockstudio\Attributes\Cron- global helpers like
bs_render_block()
Legacy compatibility aliases are stubbed too, so older codebases still analyze cleanly while migrating.
Injected template variables
Blockstudio includes PHP block templates from its own render function, so PHPStan cannot see where their variables come from. The extension types them directly, with no annotation required:
| Variable | Type |
|---|---|
$attributes, $a |
array shape built from block.json, or array<string, mixed> when the block declares no attributes |
$block, $b |
array<string, mixed> |
$context, $c |
array<string, mixed> |
$content, $inner_blocks |
string |
$islandPhase |
string |
$isEditor, $isPreview, $isIsland, $isIslandPlaceholder, $isIslandFragment |
bool |
// blockstudio/hero/index.php <?php echo $a['title']; // string echo $a['cta']['href']; // string
A @var array<string, mixed> $a annotation overrides the inferred shape, so
remove any that were added before this typing existed. Assignments made by the
template always keep their own inferred type.
Configuration
The extension requires no manual configuration. It auto-discovers
block.json, db.php, rpc.php, cron.php, page.json, field.json, and
blockstudio.json files in your project.
If a project references blocks or reusable fields from a library outside the
project root, add the library directory to blockstudioScanRoots. Both
block.json and field.json files are indexed:
parameters: blockstudioScanRoots: - vendor/acme/block-library/blockstudio
If you need to exclude specific paths, use PHPStan's standard excludePaths:
parameters: excludePaths: - some/path/to/exclude
Opt-in analysis presets
The auto-discovered extension.neon remains the compatibility-safe base
extension. Version 7.6 adds separate presets; installing the package does not
enable them automatically.
| Preset | Includes |
|---|---|
base.neon |
The unchanged auto-discovered schema, template, hook, and API checks. |
theme.neon |
Base plus WordPress theme structure, Blockstudio asset references, scoped block styles, field defaults, and repeater bounds. |
extreme-theme.neon |
Theme plus PHPStan max, unsafe PHP checks, output escaping, Tailwind validation, JavaScript syntax and browser hygiene, and Interactivity API contracts. |
wordpress-render.neon |
Extreme theme plus an explicit live WordPress render probe. It never starts WordPress by itself. |
Include a preset directly when an existing PHPStan command owns the rest of the configuration:
includes: - vendor/blockstudio/phpstan/extreme-theme.neon parameters: blockstudioThemeRoots: - . blockstudioThemeExcludePaths: - fixtures/** blockstudioThemeMaxFiles: 10000 blockstudioExtremeJavaScript: true blockstudioExtremeTailwind: true
blockstudioThemeExcludePaths limits the project scanner. PHPStan's own
excludePaths still controls which PHP files PHPStan analyzes.
When the canonical command reads phpstan.excludePaths from
blockstudio.json (or receives --exclude), it applies each exclusion to both
the Blockstudio project scanner and PHPStan analysis. Relative patterns resolve
against every configured root even though the generated NEON file lives in the
system temporary directory.
Canonical command
The package installs vendor/bin/blockstudio-phpstan. It defaults to the
extreme-theme preset:
vendor/bin/blockstudio-phpstan --root . -- --no-progress
The canonical command gives PHPStan a 1G memory limit by default so a normal
PHP CLI 128M limit does not fail on Composer-managed WordPress projects.
Override it for a specific run after --, for example
-- --memory-limit=512M --no-progress.
Projects can keep the canonical command's defaults in blockstudio.json:
{
"$schema": "https://blockstudio.dev/schema/blockstudio",
"phpstan": {
"preset": "extreme-theme",
"roots": ["."],
"excludePaths": ["fixtures/**"],
"maxFiles": 10000
}
}
Relative roots resolve from the configuration file. Explicit command-line
values replace their corresponding JSON value, so automation can make a
one-off selection without rewriting project configuration. Use
--blockstudio-json <path> when a project supplies the settings from an
alternate source. Invalid JSON, unknown phpstan keys, invalid types, and a
missing explicit source fail with exit code 2.
Use another preset, compose a project configuration, or emit PHPStan's JSON format:
vendor/bin/blockstudio-phpstan \ --preset theme \ --configuration phpstan.neon \ --root . \ --exclude 'fixtures/**' \ --error-format json \ -- --no-progress
The command writes its composed NEON file only to the system temporary directory and removes it on exit. It does not create a project cache, configuration, baseline, or hook. PHPStan's normal cache policy still applies when a caller explicitly configures one.
Exit codes are stable:
0: analysis passed1: PHPStan reported diagnostics2: invalid usage, configuration, or process execution
Managed commit hook
Blockstudio can own an analysis-only pre-commit hook. Enable it in the project
blockstudio.json:
{
"$schema": "https://blockstudio.dev/schema/blockstudio",
"phpstan": {
"preset": "extreme-theme",
"roots": ["."],
"excludePaths": [],
"maxFiles": 10000
},
"githooks": {
"commit": true
}
}
Then synchronize the repository:
vendor/bin/blockstudio-githooks sync
The command installs a generated hook inside Git's common directory and points
core.hooksPath at its managed directory. It records the prior hooks path and
chains an existing pre-commit hook before running
vendor/bin/blockstudio-phpstan from the configured project root. The
canonical command reads the same phpstan object, so the generated hook needs
no duplicated arguments. Re-running the command refreshes an owned hook,
including after package upgrades.
Set commit to false, remove the setting, or remove blockstudio.json, then
run sync again to remove only Blockstudio-owned files and restore the recorded
hooks path. vendor/bin/blockstudio-githooks remove performs the same safe
cleanup explicitly.
Blockstudio refuses to overwrite or remove files without its generated marker.
If core.hooksPath is changed after installation, removal leaves that newer
user setting intact. Linked Git checkouts share the managed directory, while
the generated hook resolves the active checkout and project root at commit
time. Paths containing spaces are supported.
The hook performs PHPStan analysis only. It never formats or rewrites project
files. A missing Composer installation or failed analysis blocks the commit
with a direct error; bypass behavior remains Git's standard --no-verify.
Project contract
vendor/bin/blockstudio-agents writes an AGENTS.md describing the project it
runs in, for the coding agents that read one before touching a codebase:
vendor/bin/blockstudio-agents
The document is generated, never templated. The project's own files decide
whether it describes a theme that authors blocks and pages or a plugin that
consumes Blockstudio, and they supply the counts, directories, namespaces, and
template languages. blockstudio.json decides which features are described and
with which values. The selected preset decides the correctness section, which is
read from the preset files themselves, so a layer that gains a rule changes the
generated document with it. The commands section lists only the commands that
apply to that project.
--root <path> Project root (default: current directory)
--config <path> blockstudio.json path (default: <root>/blockstudio.json)
--output <path> Contract path (default: <root>/AGENTS.md)
--stdout Print the contract instead of writing it
--check Fail when the contract on disk is not current
--force Replace a file Blockstudio does not own
Ownership follows the commit hook: the generated file carries a marker, and a
file without it is never replaced. --check exits 1 on an outdated contract
so CI can gate it. The notes region at the end of the document is author owned,
and its bytes survive every regeneration.
Optional live WordPress render
Live rendering is a separate, explicit preset because it is slower and needs a caller-owned WordPress environment:
vendor/bin/blockstudio-phpstan \ --preset wordpress-render \ --render-command='["wp","eval-file","tools/render-probe.php"]' \ --render-working-directory=. \ --render-timeout=60 \ --root .
The command is an argv JSON array and is executed without a shell. The probe must print exactly one JSON object:
{"ok":true}
To report a failure:
{
"ok": false,
"message": "Rendered block failed.",
"file": "/absolute/path/to/block.json",
"line": 12
}
Non-zero exits, timeouts, malformed JSON, and an ok: false response use the
blockstudio.wordpress.render diagnostic.
Preset diagnostics
Every new diagnostic has a stable blockstudio.* identifier:
- Theme roots and structure:
blockstudio.theme.root.missing,blockstudio.theme.style.missing,blockstudio.theme.style.header, andblockstudio.theme.scanLimit - Block and field contracts:
blockstudio.theme.asset.manualEnqueue,blockstudio.theme.asset.selectorScope,blockstudio.theme.asset.missing,blockstudio.field.default, andblockstudio.field.repeaterBounds - PHP:
blockstudio.php.forbiddenFunction,blockstudio.wordpress.rawDatabaseWrite, andblockstudio.output.unescaped - Tailwind:
blockstudio.tailwind.compilerMissing,blockstudio.tailwind.compile,blockstudio.tailwind.unknownUtility, andblockstudio.tailwind.semanticToken - JavaScript:
blockstudio.javascript.syntax,blockstudio.javascript.debugOutput,blockstudio.javascript.bannedApi,blockstudio.javascript.leakedGlobal,blockstudio.javascript.importSpecifier,blockstudio.javascript.rootGuard,blockstudio.javascript.initShape,blockstudio.javascript.domContract,blockstudio.javascript.listenerCleanup, andblockstudio.javascript.reducedMotion - Interactivity API:
blockstudio.interactivity.import,blockstudio.interactivity.moduleImport,blockstudio.interactivity.namespace,blockstudio.interactivity.scopedDom,blockstudio.interactivity.derivedState,blockstudio.interactivity.handler,blockstudio.interactivity.binding,blockstudio.interactivity.context, andblockstudio.interactivity.orphan
Performance
The theme scanner uses ordinary materialized filesystem roots, deduplicates
them, skips dependency/build/cache trees by default, reads each file once, and
sorts diagnostics deterministically. Use narrow
blockstudioThemeRoots/--root values, exclusions, and
blockstudioThemeMaxFiles/--max-files for large repositories. JavaScript and
Tailwind checks can be disabled independently. The live-render layer runs only
when explicitly selected.
Requirements
- PHP 8.2+
- PHPStan 2.0+
- phpstan/phpstan-wordpress
- Phasis for JavaScript parsing
- BCMath compatibility for hosts
without the native
ext-bcmathextension - TailwindPHP for Tailwind analysis
License
MIT