IDE and static analysis#
ACF Chef field verbs resolve through __call().
PHP can execute a registered verb at run time, but an editor or static analyser cannot discover that dynamic registry on its own. ACF Chef deliberately does not maintain a parallel list of @method annotations that can drift away from the real registry.
Instead, generate type information from the registry your application actually boots.
Generate an editor stub#
Run:
vendor/bin/acf-chef ide-stub
By default, the command generates the editor flavour at:
.acf-chef-stub.php
The available options are:
--output=PATH
--for=TARGET
--bootstrap=PATH
-h
--help
--for accepts:
editor
phpstan
editor is the default.
Any other target fails with:
acf-chef: --for takes `editor` or `phpstan`.
An empty output path fails with:
acf-chef: --output needs a path.
Bootstrap your project registrations#
The generated stub reflects the live registry.
If your project registers additional field types, point --bootstrap at the PHP file that performs those registrations:
vendor/bin/acf-chef ide-stub --bootstrap=app/fields.php
The bootstrap file is required before the command generates anything, so calls such as:
Chef::register('acfeColumn', AcfeColumn::class);
have already run when the registry is inspected.
Without that bootstrap, the generated file contains only the verbs ACF Chef registers itself.
--bootstrap must point at a readable PHP file. Otherwise the command fails with:
acf-chef: --bootstrap needs a readable PHP file.
If a custom verb is missing from the generated stub, first check that its registration code ran during the CLI bootstrap.
Editor and PHPStan stubs are different#
The two generated flavours serve different consumers.
The editor flavour declares five types: the three concrete builder classes and the two container/cursor contracts. Editors index declarations they find, so this gives completion across the normal fluent API.
The PHPStan flavour is deliberately narrower. It declares the relevant contracts plus a bare Recipe.
That difference avoids replacing class-level metadata PHPStan still needs from the real classes. A PHPStan stub replaces the declaration information for any type it names; stubbing FieldGroup, for example, would discard its consistent-constructor annotation and break analysis of make().
Recipe still needs a declaration in the analysis stub because container verbs accept it as their second argument. Without one, those generated signatures would refer to an unknown type.
Never autoload a generated stub#
A stub is for an editor or analyser to inspect. It is not application code.
It redeclares types that already exist, so requiring it at run time causes a fatal redeclaration error.
The generated file guards itself with an exit() at the top, and the default .acf-chef-stub.php location sits outside normal PSR-4 source roots, but your project should still keep the distinction explicit:
- Do not add the stub to Composer autoloading.
- Keep it outside PSR-4 roots.
- Add generated stubs to
.gitignoreunless your project deliberately commits one. - Regenerate after changing the field registry.
PHPStan#
Generate the PHPStan flavour separately:
vendor/bin/acf-chef ide-stub \
--for=phpstan \
--output=stubs/acf-chef.stub \
--bootstrap=app/fields.php
Register that file in phpstan.neon:
parameters:
stubFiles:
- stubs/acf-chef.stub
The stub teaches PHPStan the dynamic verbs that existed in the registry when it was generated.
There is an intentional consequence: a stub replaces the type declaration it describes, so __call() does not provide a fallback for verbs missing from the stub.
If you register another verb and forget to regenerate, PHPStan reports an undefined method rather than quietly accepting an untyped dynamic call.
That failure is useful, but it means the generated stub has to stay in sync with the registry.
Keep a committed stub honest#
If your project chooses to commit its PHPStan stub, generate it rather than editing it by hand.
ACF Chef itself follows that pattern: its committed analysis stub is produced by the command, and a test asserts that the committed file still matches the generator output.
That turns stale generated type information into a test failure instead of allowing it to drift silently.
The extended-acf macros use a separate stub#
ACF Chef also installs conditional() and orWhere() as macros on extended-acf field objects for use inside deferred configuration closures.
Those methods are documented through a separate hand-written macros stub.
That file is not produced by ide-stub, and unlike the generated PHPStan stub, it has no generator-drift test.
The distinction is intentional: ide-stub reflects the dynamic field verb registry. It does not discover arbitrary macros.
The same limitation applies to cursor macros you register yourself with Chef::macro(). They do not appear in the generated stub, so an editor or static analyser will not learn about them automatically.
See Macros and shorthands for that limitation.
Use add() without a generated verb stub#
If you do not want generated type information, use the declared add() method:
$set->add('acfeColumn', 'column_a');
At run time, this resolves the same registered field type as:
$set->acfeColumn('column_a');
The difference is visible to static analysis: add() is a real declared method, so PHPStan does not need a generated verb signature to accept the call.
Use the fluent verb when generated completion is useful. Use add() when the verb is data or when you want the definition to remain analysis-safe without generated stubs.
Inspect the live registry#
To see what the CLI process actually registered, run:
vendor/bin/acf-chef verbs
It prints every currently registered verb.
This is usually the fastest way to diagnose a missing generated method. If the verb is absent from this list, the code that calls Chef::register() did not run in that CLI process.
Use the same bootstrap when inspecting project registrations:
vendor/bin/acf-chef verbs --bootstrap=app/fields.php
The bootstrap is required before the command runs, just as it is for ide-stub.
Running the CLI with no command prints its usage information and exits successfully rather than treating the missing command as an error.
See Registering field types for the runtime registry itself.