Skip to content

Improve Twig-sourced code generation and restrict usage#39

Merged
ruudk merged 6 commits intomainfrom
restrict-twig
Apr 17, 2026
Merged

Improve Twig-sourced code generation and restrict usage#39
ruudk merged 6 commits intomainfrom
restrict-twig

Conversation

@ruudk
Copy link
Copy Markdown
Owner

@ruudk ruudk commented Apr 17, 2026

Exclude Generated dirs from php-cs-fixer hook

Lefthook invokes php-cs-fixer with explicit {staged_files}, which overrides the Finder-level notPath(['Generated']) exclusion in .php-cs-fixer.php. That let the fixer reformat generated fixtures (stripping blank lines between class members) whenever any file inside a Generated/ dir was staged, causing the generator and fixer to fight each turn.

Add a hook-level exclude: ["**/Generated/**"] so generated output is left untouched — it's regenerated by the tool, not hand-edited.

Restrict Twig-sourced generated classes

Split the single FileSource into GraphQLFileSource (for .graphql files) and TwigFileSource (for .twig templates) so the generator can distinguish them and treat them differently.

Twig templates render by having the framework instantiate the generated fragment classes for them — userland code shouldn't be constructing or subclassing these. Mark Twig-sourced classes with restricted: true, restrictInstantiation: true on the #[Generated] attribute so the PHPStan RestrictedUsageExtension blocks direct use outside the generating template. .graphql-sourced operations stay unrestricted since they're meant to be called explicitly from application code.

OperationClassPlan keeps the narrower GraphQLFileSource | InlineSource union because Twig templates only contain fragment definitions, never operations; planOperation throws early if a Twig source ever reaches it.

Add functional tests for RestrictedUsageExtension

The PHPStan extension that enforces #[Generated(restricted: true, restrictInstantiation: true)] had no direct coverage — only implicit via fixtures asserting the attribute was emitted. A regression in the extension itself (wrong scope check, missing namespace filter, broken attribute parsing) would pass every existing test.

Add a second PHPStan run driven by tests/PHPStan/phpstan.neon, invoked as a regular PHPUnit test so it runs as part of the normal test suite:

  • tests/PHPStan/Generated/{Data,SomeQuery}.php — fake generated classes with #[Generated] pointing at AllowedController as the source. Data is fully restricted; SomeQuery is restricted but not restrictInstantiation, so anyone can new it.
  • tests/PHPStan/Fixtures/AllowedController.php — the declared source; must produce zero restricted-usage errors.
  • tests/PHPStan/Fixtures/NotAllowedController.php — any other caller; must produce exactly four errors (method, instantiation, and two property accesses).
  • tests/PHPStan/RestrictedUsageExtensionTest.php — single PHPUnit test that shells out to vendor/bin/phpstan with the test neon and asserts exit 0.

Expected errors live in the neons ignoreErrorsblock with exact messages andcount:values. Combined withreportUnmatchedIgnoredErrors` (default true), the run fails if any expected error stops firing or an unexpected one appears.

Silence shipmonk.deadMethod for the fixture dir in the root phpstan.php so the main run stays clean.

ruudk added 6 commits April 16, 2026 20:29
Lefthook invokes php-cs-fixer with explicit `{staged_files}`, which
overrides the Finder-level `notPath(['Generated'])` exclusion in
`.php-cs-fixer.php`. That let the fixer reformat generated fixtures
(stripping blank lines between class members) whenever any file
inside a `Generated/` dir was staged, causing the generator and
fixer to fight each turn.

Add a hook-level `exclude: ["**/Generated/**"]` so generated output
is left untouched — it's regenerated by the tool, not hand-edited.
Split the single `FileSource` into `GraphQLFileSource` (for
`.graphql` files) and `TwigFileSource` (for `.twig` templates) so
the generator can distinguish them and treat them differently.

Twig templates render by having the framework instantiate the
generated fragment classes for them — userland code shouldn't be
constructing or subclassing these. Mark Twig-sourced classes with
`restricted: true, restrictInstantiation: true` on the
`#[Generated]` attribute so the PHPStan `RestrictedUsageExtension`
blocks direct use outside the generating template. `.graphql`-sourced
operations stay unrestricted since they're meant to be called
explicitly from application code.

`OperationClassPlan` keeps the narrower `GraphQLFileSource |
InlineSource` union because Twig templates only contain fragment
definitions, never operations; `planOperation` throws early if a
Twig source ever reaches it.
The PHPStan extension that enforces `#[Generated(restricted: true,
restrictInstantiation: true)]` had no direct coverage — only
implicit via fixtures asserting the attribute was emitted. A
regression in the extension itself (wrong scope check, missing
namespace filter, broken attribute parsing) would pass every
existing test.

Add a second PHPStan run driven by `tests/PHPStan/phpstan.neon`,
invoked as a regular PHPUnit test so it runs as part of the normal
test suite:

- `tests/PHPStan/Generated/{Data,SomeQuery}.php` — fake generated
  classes with `#[Generated]` pointing at `AllowedController` as
  the source. `Data` is fully restricted; `SomeQuery` is restricted
  but not `restrictInstantiation`, so anyone can `new` it.
- `tests/PHPStan/Fixtures/AllowedController.php` — the declared
  source; must produce zero restricted-usage errors.
- `tests/PHPStan/Fixtures/NotAllowedController.php` — any other
  caller; must produce exactly four errors (method, instantiation,
  and two property accesses).
- `tests/PHPStan/RestrictedUsageExtensionTest.php` — single
  PHPUnit test that shells out to `vendor/bin/phpstan` with the
  test neon and asserts exit 0.

Expected errors live in the neon`s `ignoreErrors` block with exact
messages and `count:` values. Combined with
`reportUnmatchedIgnoredErrors` (default true), the run fails if
any expected error stops firing or an unexpected one appears.

Silence `shipmonk.deadMethod` for the fixture dir in the root
`phpstan.php` so the main run stays clean.
@ruudk ruudk merged commit bdcba84 into main Apr 17, 2026
5 checks passed
@ruudk ruudk deleted the restrict-twig branch April 17, 2026 08:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant