Blockstudio
Dev

PHPStan

The blockstudio/phpstan extension brings static analysis to Blockstudio projects. It validates block templates, schema files, settings paths, and hook names at analysis time, catching bugs before they reach runtime.

Installation

composer require --dev blockstudio/phpstan

The extension auto-discovers via PHPStan's extension installer. No manual configuration needed.

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
/** @var array<string, mixed> $a */

echo $a['title'];     // OK
echo $a['subtitle'];  // OK
echo $a['typo'];      // Error: Field "typo" does not exist in block.json

Add /** @var array<string, mixed> $a */ at the top of each PHP template so PHPStan knows $a exists. The extension handles the rest.

Twig template access

Twig templates are scanned automatically. No annotation needed.

blockstudio/hero/index.twig
<h1>{{ a.title }}</h1>
<p>{{ a.typo }}</p>    {# Error: Field "typo" does not exist in block.json #}

Blade template access

Blade templates are scanned the same way.

blockstudio/hero/index.blade.php
<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 title="Hi" badattr="" />  <!-- Error: unknown attribute -->
<block name="core/separator" />            <!-- OK (core blocks always valid) -->

Attributes with data-* and html-* prefixes are pass-through and never checked.

Database record typing

Db::get() returns a typed instance based on the db.php schema. Record shapes are inferred automatically.

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 (optional field)
echo $record['typo'];  // Error: Offset 'typo' does not exist

Required fields are non-nullable. Optional fields are type|null. An id field (int) is always present. The same typing works for the PHP-native Blockstudio\\Db\\Schema / Blockstudio\\Db\\Field builder syntax.

Settings path validation

Settings::get() paths are checked against the known settings schema.

Settings::get('tailwind/enabled');  // OK, returns bool
Settings::get('tailwind/enabld');   // Error: Did you mean "tailwind/enabled"?

Hook name validation

Blockstudio filter and action hook names are validated.

add_filter('blockstudio/render', $cb);   // OK
add_filter('blockstudio/rendrr', $cb);   // Error: Did you mean "blockstudio/render"?

Dynamic settings hooks like blockstudio/settings/tailwind/enabled are always allowed. Non-blockstudio hooks are ignored.

Schema validation

The extension validates every Blockstudio schema file in the project.

block.json:

  • Missing name field
  • Missing field type
  • Missing id on value fields that require one
  • Unknown field types
  • select/radio/checkbox without options or populate
  • Duplicate field IDs

Container fields like group and tabs, plus custom/* field references, can omit id when they only expand or wrap other fields.

field.json (custom reusable fields):

  • Missing name
  • Missing field type
  • Missing id on value fields that require one
  • Unknown field types

Extension JSON (block extensions in extensions/):

  • Missing name (target block)
  • Missing blockstudio key
  • Missing blockstudio.extend key

page.json (file-based pages):

  • Missing title
  • Missing slug
  • Invalid postStatus values

db.php:

  • Missing fields array
  • Invalid field types
  • Supports both legacy arrays and Blockstudio\\Db\\Schema / Blockstudio\\Db\\Field

rpc.php:

  • Invalid HTTP methods
  • Wrong public value (must be true, false, or 'open')
  • Supports both legacy arrays and attributed object returns

cron.php:

  • Missing schedule on configured tasks
  • Missing callback on configured tasks
  • Supports both legacy arrays and attributed object returns

blockstudio.json:

  • Shorthand booleans like "tailwind": true (must use the nested object format {"enabled": true, "config": ""})

Field type shapes

The extension maps every Blockstudio field type to a concrete PHP type:

Field typePHP type
text, textarea, richtext, wysiwyg, codestring
date, datetime, classes, html-tag, unit, gradientstring
number, rangeint|float
togglebool
select, radio, checkbox (single)string|int
select, checkbox (multiple)list<string|int>
colorarray{value: string, opacity: float|null}
linkarray{href: string, title: string|null, target: string|null, opensInNewTab: bool|null}
iconarray{set: string, subSet: string, icon: string}
files (single)array{id: int, url: string, alt: string|null, mime_type: string|null}
files (multiple)list<array{id: int, url: string, ...}>
groupFlattened with underscore prefix: cta_text, cta_url
repeaterlist<array{...child fields...}>
tabsFlattened into parent scope

API stubs

The extension ships stubs for the full Blockstudio public API:

  • bs_render_block(), bs_get_group(), bs_get_scoped_class(), bs_db_form()
  • Db::get(), Db::create(), Db::list(), Db::get_record(), Db::update(), Db::delete()
  • Settings::get(), Settings::get_all()
  • Field_Registry::instance(), Field_Registry::all(), Field_Registry::get()
  • Build::blocks(), Build::extensions(), Build::get_build_dir()

These provide autocomplete and type checking without needing the Blockstudio plugin source on your machine.

Requirements

  • PHP 8.2+
  • PHPStan 2.0+
  • phpstan/extension-installer (recommended, for auto-discovery)

On this page