Models
Annotate Tables and their Entities:
bin/cake annotate modelsTables
Tables annotate their entity-related methods, their relations, and behavior mixins.
A LocationsTable class would gain the following doc-block annotations if not already present:
/**
* @method \App\Model\Entity\Location newEmptyEntity()
* @method \App\Model\Entity\Location newEntity(array $data, array $options = [])
* @method array<\App\Model\Entity\Location> newEntities(array $data, array $options = [])
* @method \App\Model\Entity\Location get(mixed $primaryKey, array|string $finder = 'all', \Psr\SimpleCache\CacheInterface|string|null $cache = null, \Closure|string|null $cacheKey = null, mixed ...$args)
* @method \Cake\ORM\Query\SelectQuery<\App\Model\Entity\Location> find(string $type = 'all', mixed ...$args)
* @method \App\Model\Entity\Location|false save(\Cake\Datasource\EntityInterface $entity, array $options = [])
* @method \App\Model\Entity\Location saveOrFail(\Cake\Datasource\EntityInterface $entity, array $options = [])
* @method \App\Model\Entity\Location patchEntity(\Cake\Datasource\EntityInterface $entity, array $data, array $options = [])
* @method array<\App\Model\Entity\Location> patchEntities(iterable $entities, array $data, array $options = [])
* @method \App\Model\Entity\Location findOrCreate(\Cake\ORM\Query\SelectQuery|callable|array $search, ?callable $callback = null, array $options = [])
* @method \Cake\Datasource\ResultSetInterface<int, \App\Model\Entity\Location>|false saveMany(iterable $entities, array $options = [])
* @method \Cake\Datasource\ResultSetInterface<int, \App\Model\Entity\Location> saveManyOrFail(iterable $entities, array $options = [])
* @method \Cake\Datasource\ResultSetInterface<int, \App\Model\Entity\Location>|false deleteMany(iterable $entities, array $options = [])
* @method \Cake\Datasource\ResultSetInterface<int, \App\Model\Entity\Location> deleteManyOrFail(iterable $entities, array $options = [])
*
* @property \Cake\ORM\Association\HasMany<\App\Model\Table\ImagesTable> $Images
* @property \Cake\ORM\Association\BelongsTo<\App\Model\Table\UsersTable> $Users
*
* @mixin \Cake\ORM\Behavior\TimestampBehavior
*/Entity-aware find() return type
If you want the table annotations to also expose the entity-aware find() return type for IDEs, enable:
'IdeHelper' => [
'tableEntityQuery' => true,
],This is intentionally optional, because finder result shapes can still widen beyond plain entities.
Detailed param types
The IdeHelper.genericsInParam option is tri-state:
false(default) — barearrayparams, legacy behavior. Collection return types still carryResultSetInterface<int, TEntity>(its real two-param arity).true— basic generics. Entity-data and$containparams usearray<mixed>($containalso accepts the list form['Comments', 'Tags']); the$finder,$search, and$optionsparams usearray<string, mixed>, alongsideSelectQuery<TEntity>/iterable<TEntity>/ResultSetInterface<int, TEntity>. No param stays a barearrayand no generic class is left unparametrized, so PHPStan'smissingType.iterableValueandmissingType.genericsstay clean at the strictest level - while IDE/tooling still copes, since there are no nestedarray<array<...>>shapes.'detailed'— fully detailed types throughout (nestedarray<array<string, mixed>>for entity lists), matching the richer form PHPStan and Psalm understand best.
With 'detailed', the generated method annotations look like:
* @method \App\Model\Entity\User newEntity(array<string, mixed> $data, array<string, mixed> $options = [])
* @method array<\App\Model\Entity\User> newEntities(array<array<string, mixed>> $data, array<string, mixed> $options = [])
* @method \App\Model\Entity\User get(mixed $primaryKey, array<string, mixed>|string $finder = 'all', \Psr\SimpleCache\CacheInterface|string|null $cache = null, \Closure|string|null $cacheKey = null, mixed ...$args)
* @method \App\Model\Entity\User findOrCreate(\Cake\ORM\Query\SelectQuery<\App\Model\Entity\User>|callable|array<string, mixed> $search, ?callable $callback = null, array<string, mixed> $options = [])
* @method \Cake\Datasource\ResultSetInterface<int, \App\Model\Entity\User>|false saveMany(iterable<\App\Model\Entity\User> $entities, array<string, mixed> $options = [])Switching the value only tightens the generated types; re-running the annotator updates the existing @method lines in place, and the 'detailed' opt-in can be enabled at any time.
Concrete entity types in params
The IdeHelper.concreteEntitiesInParam option is tri-state:
false(default) — entity params accept\Cake\Datasource\EntityInterface.true—patchEntity(),save(), andsaveOrFail()declare the concrete entity class for their$entityparameter, so PHPStan can catch a mismatched entity passed to the wrong table.'strict'— same astrue, plus:- iterable params on
patchEntities/saveMany/saveManyOrFail/deleteMany/deleteManyOrFailare narrowed toiterable<\App\Model\Entity\X>even whengenericsInParamisfalse. delete(),deleteOrFail(), andloadInto()get explicit@methodlines typed with the concrete entity class.
- iterable params on
This mirrors the runtime check proposed upstream in cakephp/cakephp#19428: a Table::delete($order) call against an InvoicesTable is rejected at static-analysis time instead of silently deleting from the wrong table.
With 'strict' (and genericsInParam left at default false), the generated method block adds:
* @method \App\Model\Entity\User patchEntity(\App\Model\Entity\User $entity, array $data, array $options = [])
* @method \App\Model\Entity\User[] patchEntities(iterable<\App\Model\Entity\User> $entities, array $data, array $options = [])
* @method bool delete(\App\Model\Entity\User $entity, array $options = [])
* @method bool deleteOrFail(\App\Model\Entity\User $entity, array $options = [])
* @method \App\Model\Entity\User|array<\App\Model\Entity\User> loadInto(\App\Model\Entity\User|array<\App\Model\Entity\User> $entities, array $contain)Switching is additive — existing true users keep their current output, and 'strict' can be enabled at any time. Pair with genericsInParam at true or 'detailed' for fully typed params throughout.
Entities
Entities annotate their properties and relations.
A Location entity could look like this afterward:
/**
* @property int $id
* @property int $user_id
* @property \App\Model\Entity\User $user
* @property string $location
* @property string $details
* @property \Cake\I18n\DateTime $created
* @property \Cake\I18n\DateTime $modified
*
* @property \App\Model\Entity\Image[] $images
* @property \App\Model\Entity\User $user
*
* @property-read string|null $virtual_property
*/
class Location extends Entity {
}Custom type maps
Using the Configure key 'IdeHelper.typeMap' you can set a custom array of types to be used for the field mapping. Overwriting the defaults of this plugin is also possible — to skip (reset) just set the value to null:
'IdeHelper' => [
'typeMap' => [
'custom' => 'array',
'longtext' => null,
// ...
],
],Using the Configure key 'IdeHelper.nullableMap' you can set a custom array of types and whether they can be nullable:
'IdeHelper' => [
'nullableMap' => [
'custom' => false,
'longtext' => true,
// ...
],
],Custom enum types
Columns mapped to a backed enum via EnumType::from(MyEnum::class) are detected automatically and annotated with the concrete enum class, e.g. @property \App\Model\Enum\MyEnum|null $status.
A custom EnumType subclass (for example a lenient type that tolerates legacy values) registered under its own name is not detected out of the box, because the annotator only sees the type name from the schema and cannot tell that name maps to an enum — it falls back to string:
// config/bootstrap.php
TypeFactory::map('lenient_status', LenientStatusType::class); // extends EnumType
// Table::initialize()
$this->getSchema()->setColumnType('status', 'lenient_status');
// => @property string|null $status (enum class lost)Register it under an enum- prefixed name so the resolver recognises it as an enum and reads the backing class via getEnumClassName() — no typeMap entry needed:
TypeFactory::map('enum-lenient_status', LenientStatusType::class);
$this->getSchema()->setColumnType('status', 'enum-lenient_status');
// => @property \App\Model\Enum\MyStatus|null $statusThe only requirement is that the registered type name starts with enum- and the type resolves to a \Cake\Database\Type\EnumType instance. (Alternatively, map the custom name to the enum class via IdeHelper.typeMap.)
Virtual properties
For virtual properties the annotator looks up the respective _get...() methods (e.g. _getVirtualProperty() for $virtual_property). It first checks the documented type in the doc block's @return, otherwise (given PHP 7.0+) tries to read it from the return type hint (e.g. : ?string). Only if that is also not present does it fall back to mixed.
Pure virtual fields are annotated with the @property-read tag instead of @property: a field counts as read-only when it has a _get...() accessor but neither a real DB column / association behind it nor a matching _set...() mutator. If you do add a _set...() mutator (or the name also maps to a real column), the field stays writable and keeps the plain @property tag.