From 677c6d240e70e48681114322fc4687d58f136506 Mon Sep 17 00:00:00 2001 From: Andrew Riddlestone Date: Sat, 4 Apr 2026 08:28:27 +0100 Subject: [PATCH 1/8] Ignore CakePHP audit issues --- composer.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/composer.json b/composer.json index 1d3c7b1..18ea374 100644 --- a/composer.json +++ b/composer.json @@ -63,6 +63,12 @@ "config": { "allow-plugins": { "dealerdirect/phpcodesniffer-composer-installer": true + }, + "audit": { + "ignore": { + "CVE-2015-8379": "CakePHP code is tested, not run", + "CVE-2020-15400": "CakePHP code is tested, not run" + } } } } From e0e797b83909c8a20855ffc2f00f23d4ff7c044e Mon Sep 17 00:00:00 2001 From: Andrew Riddlestone Date: Sat, 4 Apr 2026 10:16:40 +0100 Subject: [PATCH 2/8] Updated to PHPStan 2.x --- composer.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 18ea374..f83c342 100644 --- a/composer.json +++ b/composer.json @@ -11,11 +11,11 @@ ], "require": { "php": "^7.4", - "phpstan/phpstan": "^1.9" + "phpstan/phpstan": "^2" }, "require-dev": { "cakephp/cakephp": "^2.10.24", - "phpstan/phpstan-phpunit": "^1.3", + "phpstan/phpstan-phpunit": "^2", "phpunit/phpunit": "^9.6", "nunomaduro/phpinsights": "^2.7" }, From 32032f4d907e05950e06a3e53d5ddcd5b8e3b94f Mon Sep 17 00:00:00 2001 From: Andrew Riddlestone Date: Sat, 4 Apr 2026 12:02:04 +0100 Subject: [PATCH 3/8] chore: Various PHPInsights improvements --- phpinsights.php | 1 + src/ClassReflectionFinder.php | 5 +- src/ClassRegistryInitExtension.php | 34 +++++-- ...omponentOnFlyMethodReturnTypeExtension.php | 21 +++-- src/Service/SchemaService.php | 88 +++++++++++++------ 5 files changed, 105 insertions(+), 44 deletions(-) diff --git a/phpinsights.php b/phpinsights.php index afe89f2..5f413c2 100644 --- a/phpinsights.php +++ b/phpinsights.php @@ -55,6 +55,7 @@ 'exclude' => [ 'phpstan', + 'stubs', 'tests', 'vendor', ], diff --git a/src/ClassReflectionFinder.php b/src/ClassReflectionFinder.php index a4be4ef..d32e96c 100644 --- a/src/ClassReflectionFinder.php +++ b/src/ClassReflectionFinder.php @@ -66,7 +66,10 @@ private function getClassNamesFromPaths( } $classPaths = array_merge($classPaths, $filePaths); } - $classNames = array_map($pathToClassName ?? [$this, 'getClassNameFromFileName'], $classPaths); + $classNames = array_map( + $pathToClassName ?? [$this, 'getClassNameFromFileName'], + $classPaths + ); return array_filter( $classNames, [$this->reflectionProvider, 'hasClass'] diff --git a/src/ClassRegistryInitExtension.php b/src/ClassRegistryInitExtension.php index baf120d..612e765 100644 --- a/src/ClassRegistryInitExtension.php +++ b/src/ClassRegistryInitExtension.php @@ -1,22 +1,26 @@ reflectionProvider = $reflectionProvider; $this->schemaService = $schemaService; } @@ -35,13 +39,21 @@ public function getClass(): string return 'ClassRegistry'; } - public function isStaticMethodSupported(MethodReflection $methodReflection): bool - { + public function isStaticMethodSupported( + MethodReflection $methodReflection + ): bool { return $methodReflection->getName() === 'init'; } - public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, Scope $scope): ?Type - { + /** + * @throws ShouldNotHappenException + * @throws ConstExprEvaluationException + */ + public function getTypeFromStaticMethodCall( + MethodReflection $methodReflection, + StaticCall $methodCall, + Scope $scope + ): Type { $arg1 = $methodCall->getArgs()[0]->value; $evaluator = new ConstExprEvaluator(); $arg1 = $evaluator->evaluateSilently($arg1); @@ -54,14 +66,18 @@ public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, if ($this->schemaService->hasTable(Inflector::tableize($arg1))) { return new ObjectType('Model'); } + return $this->getDefaultType(); } + /** + * @throws ShouldNotHappenException + */ private function getDefaultType(): Type { return new UnionType([ new BooleanType(), - new ObjectWithoutClassType() + new ObjectWithoutClassType(), ]); } } diff --git a/src/LoadComponentOnFlyMethodReturnTypeExtension.php b/src/LoadComponentOnFlyMethodReturnTypeExtension.php index 060e878..c0c6d63 100644 --- a/src/LoadComponentOnFlyMethodReturnTypeExtension.php +++ b/src/LoadComponentOnFlyMethodReturnTypeExtension.php @@ -10,11 +10,11 @@ use PHPStan\Analyser\Scope; use PHPStan\Reflection\MethodReflection; use PHPStan\Reflection\ReflectionProvider; -use PHPStan\Type\DynamicMethodReturnTypeExtension; +use PHPStan\Type\DynamicMethodReturnTypeExtension as ReturnTypeExtension; use PHPStan\Type\ObjectType; use PHPStan\Type\Type; -class LoadComponentOnFlyMethodReturnTypeExtension implements DynamicMethodReturnTypeExtension +final class LoadComponentOnFlyMethodReturnTypeExtension implements ReturnTypeExtension { private ReflectionProvider $reflectionProvider; @@ -33,21 +33,28 @@ public function isMethodSupported(MethodReflection $methodReflection): bool return $methodReflection->getName() === 'load'; } - public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): ?Type - { + public function getTypeFromMethodCall( + MethodReflection $methodReflection, + MethodCall $methodCall, + Scope $scope + ): ?Type { $arg = $methodCall->getArgs()[0]->value; - if (!$arg instanceof String_) { + if (! $arg instanceof String_) { return null; } $componentName = $arg->value . 'Component'; - if (!$this->reflectionProvider->hasClass($componentName)) { + if (! $this->reflectionProvider->hasClass($componentName)) { return null; } - if (!$this->reflectionProvider->getClass($componentName)->is(Component::class)) { + if ( + ! $this->reflectionProvider + ->getClass($componentName) + ->is(Component::class) + ) { return null; } diff --git a/src/Service/SchemaService.php b/src/Service/SchemaService.php index f9890aa..906959e 100644 --- a/src/Service/SchemaService.php +++ b/src/Service/SchemaService.php @@ -1,10 +1,13 @@ + * @var list */ private array $schemaPaths; @@ -30,8 +33,12 @@ final class SchemaService private ?array $tableSchemas = null; /** - * @param ReflectionProvider $reflectionProvider - * @param array $schemaPaths + * @var list|null + */ + private ?array $cakeSchemaPropertyNames = null; + + /** + * @param list $schemaPaths */ public function __construct( ReflectionProvider $reflectionProvider, @@ -50,13 +57,14 @@ public function hasTable(string $table): bool } /** - * @param string $table * @return table_schema|null + * * @throws Exception */ public function getTableSchema(string $table) { $tableSchemas = $this->getTableSchemas(); + return array_key_exists($table, $tableSchemas) ? $tableSchemas[$table] : null; @@ -72,41 +80,67 @@ private function getTableSchemas(): array if (is_array($this->tableSchemas)) { return $this->tableSchemas; } - $cakeSchemaPropertyNames = array_map( - function (ReflectionProperty $reflectionProperty) { - return $reflectionProperty->getName(); - }, - $this->reflectionProvider->getClass('CakeSchema')->getNativeReflection()->getProperties() - ); $this->tableSchemas = []; $classReflectionFinder = new ClassReflectionFinder( - $this->reflectionProvider + $this->reflectionProvider, ); $schemaReflections = $classReflectionFinder->getClassReflections( $this->schemaPaths, 'CakeSchema', function (string $fileName) { return $this->fileNameToClassName($fileName); - } + }, ); foreach ($schemaReflections as $schemaReflection) { - $propertyNames = array_map( - function (ReflectionProperty $reflectionProperty) { - return $reflectionProperty->getName(); - }, - $schemaReflection->getNativeReflection() - ->getProperties(CoreReflectionProperty::IS_PUBLIC) - ); - $tableProperties = array_diff($propertyNames, $cakeSchemaPropertyNames); - $this->tableSchemas += array_intersect_key( - $schemaReflection->getNativeReflection()->getDefaultProperties(), - array_fill_keys($tableProperties, null) + $this->tableSchemas += $this->extractTableSchemas( + $schemaReflection, ); } return $this->tableSchemas; } + /** + * @return list + */ + private function getCakeSchemaPropertyNames(): array + { + return $this->cakeSchemaPropertyNames ??= array_map( + static function (ReflectionProperty $reflectionProperty) { + return $reflectionProperty->getName(); + }, + $this->reflectionProvider + ->getClass('CakeSchema') + ->getNativeReflection() + ->getProperties(), + ); + } + + /** + * @return array + */ + private function extractTableSchemas( + ClassReflection $schemaReflection + ): array { + $propertyNames = array_map( + static function (ReflectionProperty $reflectionProperty) { + return $reflectionProperty->getName(); + }, + $schemaReflection + ->getNativeReflection() + ->getProperties(CoreReflectionProperty::IS_PUBLIC), + ); + $tableProperties = array_diff( + $propertyNames, + $this->getCakeSchemaPropertyNames(), + ); + + return array_intersect_key( + $schemaReflection->getNativeReflection()->getDefaultProperties(), + array_fill_keys($tableProperties, null), + ); + } + private function fileNameToClassName(string $fileName): string { return str_replace( @@ -116,9 +150,9 @@ private function fileNameToClassName(string $fileName): string str_replace( ['_', '-'], ' ', - basename($fileName, '.php') - ) - ) - ) . 'Schema'; + basename($fileName, '.php'), + ), + ), + ).'Schema'; } } From 59514f2835df0ff046d454ba637320ea1628ada0 Mon Sep 17 00:00:00 2001 From: Andrew Riddlestone Date: Mon, 6 Apr 2026 12:27:43 +0100 Subject: [PATCH 4/8] Only include methods from behaviors which are in the model's actsAs property WIP --- src/ModelBehaviorsExtension.php | 110 +++++++++++++----- tests/Feature/ModelExtensionsTest.php | 1 + tests/Feature/classes/Model/BasicModel.php | 3 - .../classes/Model/ModelWithBehaviors.php | 12 ++ .../classes/Model/ModelWithoutBehaviors.php | 3 + tests/Feature/data/class_registry_init.php | 4 +- tests/Feature/data/core_model_behavior.php | 2 +- tests/Feature/data/custom_model_behavior.php | 2 +- .../data/existing_controller_model.php | 4 +- tests/Feature/data/existing_model_model.php | 2 +- tests/Feature/data/existing_shell_model.php | 4 +- tests/Feature/data/invalid_model_property.php | 2 +- tests/Feature/data/missing_model_behavior.php | 10 ++ 13 files changed, 114 insertions(+), 45 deletions(-) delete mode 100644 tests/Feature/classes/Model/BasicModel.php create mode 100644 tests/Feature/classes/Model/ModelWithBehaviors.php create mode 100644 tests/Feature/classes/Model/ModelWithoutBehaviors.php create mode 100644 tests/Feature/data/missing_model_behavior.php diff --git a/src/ModelBehaviorsExtension.php b/src/ModelBehaviorsExtension.php index f025d52..532e27c 100644 --- a/src/ModelBehaviorsExtension.php +++ b/src/ModelBehaviorsExtension.php @@ -5,6 +5,10 @@ namespace ARiddlestone\PHPStanCakePHP2; use Exception; +use Model; +use PhpParser\Node\ArrayItem; +use PhpParser\Node\Expr\Array_; +use PhpParser\Node\Scalar\String_; use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\MethodReflection; use PHPStan\Reflection\MethodsClassReflectionExtension; @@ -18,17 +22,22 @@ final class ModelBehaviorsExtension implements MethodsClassReflectionExtension /** * @var ReflectionProvider */ - private $reflectionProvider; + private ReflectionProvider $reflectionProvider; /** * @var array */ - private $behaviorPaths; + private array $behaviorPaths; /** - * @var array|null + * @var array> */ - private $behaviorMethods = null; + private array $behaviorMethods = []; + + /** + * @var array> + */ + private array $modelBehaviorNames = []; /** * @param array $behaviorPaths @@ -52,18 +61,21 @@ public function hasMethod( && in_array( $methodName, array_map( - [$this, 'getMethodReflectionName'], - $this->getBehaviorMethods() + fn(MethodReflection $methodReflection) => $methodReflection->getName(), + $this->getModelBehaviorMethods($classReflection) ) ); } + /** + * @throws Exception + */ public function getMethod( ClassReflection $classReflection, string $methodName ): MethodReflection { $methodReflections = array_filter( - $this->getBehaviorMethods(), + $this->getModelBehaviorMethods($classReflection), static function ( MethodReflection $methodReflection ) use ($methodName) { @@ -77,41 +89,75 @@ static function ( } /** - * Gets all behavior methods from known behavior classes. - * - * @return array + * @param ClassReflection $modelReflection + * @return list + */ + private function getModelBehaviorMethods(ClassReflection $modelReflection): array + { + $behaviorNames = $this->getModelBehaviorNames($modelReflection); + return array_merge(...array_map([$this, 'getBehaviorMethods'], $behaviorNames)); + } + + /** + * @param ClassReflection $modelReflection + * @return list + */ + private function getModelBehaviorNames(ClassReflection $modelReflection): array + { + if (! array_key_exists($modelReflection->getName(), $this->modelBehaviorNames)) { + $phpReflection = $modelReflection->getNativeReflection(); + if (!$phpReflection->hasProperty('actsAs')) { + // TODO merge parent class actsAs? + return []; + } + $actsAs = $phpReflection->getProperty('actsAs')->getDefaultValueExpression(); + if (!$actsAs instanceof Array_) { + return []; + } + + $this->modelBehaviorNames[$modelReflection->getName()] = array_values( + array_filter( + array_map( + fn(ArrayItem $item) => $item->value instanceof String_ ? $item->value->value : null, + $actsAs->items + ) + ) + ); + } + + return $this->modelBehaviorNames[$modelReflection->getName()]; + } + + /** + * Gets all behavior methods for a given behavior name. * + * @param string $behaviorName + * @return list * @throws Exception */ - private function getBehaviorMethods(): array + private function getBehaviorMethods(string $behaviorName): array { - if ($this->behaviorMethods === null) { + if (! array_key_exists($behaviorName, $this->behaviorMethods)) { + $this->behaviorMethods[$behaviorName] = []; + $classReflectionFinder = new ClassReflectionFinder( $this->reflectionProvider ); - $classReflections = $classReflectionFinder->getClassReflections( - $this->behaviorPaths, - 'ModelBehavior' + $reflectionClass = array_find( + $classReflectionFinder->getClassReflections( + $this->behaviorPaths, + $behaviorName.'Behavior', + ), + fn(ClassReflection $class) => $class->getName() === $behaviorName.'Behavior', ); - $this->behaviorMethods = []; - foreach ($classReflections as $classReflection) { - $modelBehaviorMethodExtractor = - new ModelBehaviorMethodExtractor($classReflection); - $this->behaviorMethods = array_merge( - $this->behaviorMethods, - $modelBehaviorMethodExtractor->getModelBehaviorMethods() - ); + if (!$reflectionClass) { + return []; } + $modelBehaviorMethodExtractor = + new ModelBehaviorMethodExtractor($reflectionClass); + $this->behaviorMethods[$behaviorName] = array_values($modelBehaviorMethodExtractor->getModelBehaviorMethods()); } - return $this->behaviorMethods; - } - /** - * Returns the name of a method from its reflection. - */ - private function getMethodReflectionName( - MethodReflection $methodReflection - ): string { - return $methodReflection->getName(); + return $this->behaviorMethods[$behaviorName]; } } diff --git a/tests/Feature/ModelExtensionsTest.php b/tests/Feature/ModelExtensionsTest.php index 60da68e..cfb7f6f 100644 --- a/tests/Feature/ModelExtensionsTest.php +++ b/tests/Feature/ModelExtensionsTest.php @@ -15,6 +15,7 @@ public function dataFileAsserts(): iterable yield from $this->gatherAssertTypes(__DIR__ . '/data/custom_model_behavior.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/invalid_model_property.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/existing_model_model.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/missing_model_behavior.php'); } /** diff --git a/tests/Feature/classes/Model/BasicModel.php b/tests/Feature/classes/Model/BasicModel.php deleted file mode 100644 index ee21f99..0000000 --- a/tests/Feature/classes/Model/BasicModel.php +++ /dev/null @@ -1,3 +0,0 @@ - + */ + public $actsAs = [ + 'Basic', + 'Translate', + ]; +} diff --git a/tests/Feature/classes/Model/ModelWithoutBehaviors.php b/tests/Feature/classes/Model/ModelWithoutBehaviors.php new file mode 100644 index 0000000..cf8f9ef --- /dev/null +++ b/tests/Feature/classes/Model/ModelWithoutBehaviors.php @@ -0,0 +1,3 @@ +bindTranslation([]); assertType('bool', $result); diff --git a/tests/Feature/data/custom_model_behavior.php b/tests/Feature/data/custom_model_behavior.php index 4f664ad..f405132 100644 --- a/tests/Feature/data/custom_model_behavior.php +++ b/tests/Feature/data/custom_model_behavior.php @@ -4,7 +4,7 @@ use function PHPStan\Testing\assertType; -/** @var BasicModel $model */ +/** @var ModelWithBehaviors $model */ $result = $model->behaviorMethod('a string!'); assertType('string', $result); diff --git a/tests/Feature/data/existing_controller_model.php b/tests/Feature/data/existing_controller_model.php index 58af979..b74bfb7 100644 --- a/tests/Feature/data/existing_controller_model.php +++ b/tests/Feature/data/existing_controller_model.php @@ -5,6 +5,6 @@ use function PHPStan\Testing\assertType; /** @var BasicController $controller */ -$model = $controller->BasicModel; +$model = $controller->ModelWithoutBehaviors; -assertType('BasicModel', $model); +assertType('ModelWithoutBehaviors', $model); diff --git a/tests/Feature/data/existing_model_model.php b/tests/Feature/data/existing_model_model.php index 680b7ec..6aa7de3 100644 --- a/tests/Feature/data/existing_model_model.php +++ b/tests/Feature/data/existing_model_model.php @@ -4,7 +4,7 @@ use function PHPStan\Testing\assertType; -/** @var BasicModel $model */ +/** @var ModelWithoutBehaviors $model */ $secondModel = $model->SecondModel; assertType('SecondModel', $secondModel); diff --git a/tests/Feature/data/existing_shell_model.php b/tests/Feature/data/existing_shell_model.php index 5eb8123..991c2b6 100644 --- a/tests/Feature/data/existing_shell_model.php +++ b/tests/Feature/data/existing_shell_model.php @@ -5,6 +5,6 @@ use function PHPStan\Testing\assertType; /** @var BasicShell $shell */ -$model = $shell->BasicModel; +$model = $shell->ModelWithoutBehaviors; -assertType('BasicModel', $model); +assertType('ModelWithoutBehaviors', $model); diff --git a/tests/Feature/data/invalid_model_property.php b/tests/Feature/data/invalid_model_property.php index 2595a9c..1808477 100644 --- a/tests/Feature/data/invalid_model_property.php +++ b/tests/Feature/data/invalid_model_property.php @@ -4,7 +4,7 @@ use function PHPStan\Testing\assertType; -/** @var BasicModel $model */ +/** @var ModelWithoutBehaviors $model */ $result = $model->unknownMethod('One', 'Two'); assertType('*ERROR*', $result); diff --git a/tests/Feature/data/missing_model_behavior.php b/tests/Feature/data/missing_model_behavior.php new file mode 100644 index 0000000..41744ef --- /dev/null +++ b/tests/Feature/data/missing_model_behavior.php @@ -0,0 +1,10 @@ +behaviorMethod('a string!'); + +assertType('*ERROR*', $result); From 1e499dea2146946da400a1ed4009c96283090d4a Mon Sep 17 00:00:00 2001 From: Andrew Riddlestone Date: Mon, 6 Apr 2026 16:54:45 +0100 Subject: [PATCH 5/8] Added docs for what we should provide in MVC class extensions --- docs/Components.md | 11 +++++++ docs/Controllers.md | 31 ++++++++++++++++++ docs/Helpers.md | 11 +++++++ docs/Models.md | 75 ++++++++++++++++++++++++++++++++++++++++++ docs/README.md | 79 +++++++++++++++++++++++++++++++++++++++++++++ docs/Shells.md | 22 +++++++++++++ 6 files changed, 229 insertions(+) create mode 100644 docs/Components.md create mode 100644 docs/Controllers.md create mode 100644 docs/Helpers.md create mode 100644 docs/Models.md create mode 100644 docs/README.md create mode 100644 docs/Shells.md diff --git a/docs/Components.md b/docs/Components.md new file mode 100644 index 0000000..ef9dbf1 --- /dev/null +++ b/docs/Components.md @@ -0,0 +1,11 @@ +# Components Extension + +Components have the following magic properties which need mapping for PHPStan: + +* [Components](#components) + +## Components + +Components listed in `$components` are available in components at `$this->{$componentName}`. + +TODO: Limit components to those listed in `$components`. diff --git a/docs/Controllers.md b/docs/Controllers.md new file mode 100644 index 0000000..69ea088 --- /dev/null +++ b/docs/Controllers.md @@ -0,0 +1,31 @@ +# Controllers Extension + +Controllers have the following magic properties which need mapping for PHPStan: + +* [Models](#models) +* [Components](#components) + +## Models + +All models listed in `$uses` are available at `$this->{$modelName}`. If `$uses` is not set, include the primary model +for the controller. If `uses` is not `false` models listed in `AppController::$uses` are also available. + +See https://book.cakephp.org/2.x/controllers.html#components-helpers-and-uses. + +TODO: Need to limit models to those listed in the controller's `$uses` property + +## Components + +All components listed in `$components` are available at `$this->{$componentName}`. If `$components` is not `false` then +components listed in `AppController::$components` are also available. + +See https://book.cakephp.org/2.x/controllers.html#components-helpers-and-uses. + +TODO: Need to limit components to those listed in the controller's `$components` property + +## Load Model + +`loadModel($modelClass)` sets `$this->{$modelClass}` to be an instance of that model class. + +TODO: Need to implement, or (if not practical to implement) add a rule to flag its usage as a failure so that it is either +set in `$uses` or replaced with a call to `ClassRegistry::init($modelClass)` to get an instance. diff --git a/docs/Helpers.md b/docs/Helpers.md new file mode 100644 index 0000000..7286fbc --- /dev/null +++ b/docs/Helpers.md @@ -0,0 +1,11 @@ +# Helpers Extension + +Helpers have the following magic properties which need mapping for PHPStan: + +* [Helpers](#helpers) + +## Helpers + +Helpers listed in `$helpers` are available in helpers at `$this->{$helperName}`. + +TODO: Limit helpers to those listed in `$helpers`. diff --git a/docs/Models.md b/docs/Models.md new file mode 100644 index 0000000..8d5d5d1 --- /dev/null +++ b/docs/Models.md @@ -0,0 +1,75 @@ +# Models Extension + +CakePHP 2 models have the following magic methods/properties which need mapping for PHPStan: + +* [Associations](#associations) +* [Behavior Methods](#behavior-methods) +* [Behavior Mapped Methods](#behavior-mapped-methods) +* [Custom Find Types](#custom-find-types) +* [Find By](#find-by) +* [Find All By](#find-all-by) +* [Find Custom By](#find-custom-by) + +## Associations + +All models listed in `$hasOne`, `$hasMany`, `$belongsTo`, and `$hasAndBelongsToMany` are available at +`$this->{$modelName}`. + +See https://book.cakephp.org/2.x/models/associations-linking-models-together.html. + +TODO: Need to limit models to those listed in the model's `$hasOne`, `$hasMany`, `$belongsTo`, and +`$hasAndBelongsToMany` properties + +## Behavior Methods + +All methods from behaviors listed in `$actsAs` are available in this model (without the first `$model` parameter). +Methods listed in `AppModel` are also available. + +See https://book.cakephp.org/2.x/models/behaviors.html. + +TODO: Need to load behaviors from the `AppModel::$actsAs` + +## Behavior Mapped Methods + +All methods from behaviors listed in `$mapMethods` are available in this model as magic methods (without the first +`$model` parameter). + +See https://book.cakephp.org/2.x/models/behaviors.html#mapped-methods. + +TODO: Need to load implement + +## Custom Find Types + +All find types listed in `$findMethods` cause the `find()` method to return the type returned by their respective +`_find{$type}` method. + +See https://book.cakephp.org/2.x/models/retrieving-your-data.html#creating-custom-find-types. + +TODO: Need to implement + +## Find By + +Methods like `findBy{$fieldName}($fieldValue, $fields, $order)` or +`findBy{$field1Name}And{$field2Name}($field1Value, $field2Name, $fields, $order)` work like `find('first')`. + +See https://book.cakephp.org/2.x/models/retrieving-your-data.html#findby. + +TODO: Need to implement + +## Find All By + +Methods like `findAllBy{$fieldName}($fieldValue, $fields, $order)` or +`findAllBy{$field1Name}And{$field2Name}($field1Value, $field2Name, $fields, $order)` work like `find('all')`. + +See https://book.cakephp.org/2.x/models/retrieving-your-data.html#findallby. + +TODO: Need to implement (probably a specific case of [Find Custom By](#find-custom-by)) + +## Find Custom By + +Methods like `findCustomBy{$fieldName}($fieldValue, $fields, $order)` or +`findCustomBy{$field1Name}And{$field2Name}($field1Value, $field2Name, $fields, $order)` work like `find('custom')`. + +See https://book.cakephp.org/2.x/models/retrieving-your-data.html#custom-magic-finders. + +TODO: Need to implement diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..891e53a --- /dev/null +++ b/docs/README.md @@ -0,0 +1,79 @@ +# Available Extensions + +## MVC Extensions + +PHPStan extensions are available for several MVC classes in CakePHP to help PHPStan understand the otherwise +undocumented methods and properties of these classes. + +```mermaid +classDiagram + namespace CakePHP { + class Shell { + <> + string[] tasks + string[] uses + } + class Model { + <> + string[] actsAs + array hasOne + array hasMany + array belongsTo + array hasAndBelongsToMany + array findMethods + array mapMethods + } + class Behavior { + <> + } + class Controller { + <> + string[] components + string[] helpers + string[] uses + } + class Component { + <> + string[] components + } + class Helper { + <> + string[] helpers + } + } + namespace App { + class AppShell { + <> + } + class AppModel { + <> + } + class AppController { + <> + } + class AppHelper { + <> + } + } + AppShell --|> Shell + Shell --> "*" Shell + Shell --> "*" Model + + AppModel --|> Model + Model --> "*" Behavior + + AppController --|> Controller + Controller --> "*" Model + Controller --> "*" Component + Controller --> "*" Helper + Component --> "*" Component + + AppHelper --|> Helper + Helper --> "*" Helper + + click Component href "Components.md" "See PHPStan customizations for Components" + click Controller href "Controllers.md" "See PHPStan customizations for Controllers" + click Helper href "Helpers.md" "See PHPStan customizations for Helpers" + click Model href "Models.md" "See PHPStan customizations for Models" + click Shell href "Shells.md" "See PHPStan customizations for Shells" +``` diff --git a/docs/Shells.md b/docs/Shells.md new file mode 100644 index 0000000..51ec9c0 --- /dev/null +++ b/docs/Shells.md @@ -0,0 +1,22 @@ +# Shells Extension + +Shells have the following magic properties which need mapping for PHPStan: + +* [Models](#models) +* [Tasks](#tasks) + +## Models + +See https://book.cakephp.org/2.x/console-and-shells.html#using-models-in-your-shells. + +All models listed in `$uses` are available at `$this->{$modelName}`. + +TODO: Need to limit models to those listed in the shell's `$uses` property + +## Tasks + +See https://book.cakephp.org/2.x/console-and-shells.html#shell-tasks. + +All tasks listed in `$tasks` are available at `$this->{$taskName}`. + +TODO: Need to limit tasks to those listed in the shell's `$tasks` property From 58a0f38b03abf5d0b90161efd7fdcf84e03a9eea Mon Sep 17 00:00:00 2001 From: Andrew Riddlestone Date: Mon, 6 Apr 2026 16:56:37 +0100 Subject: [PATCH 6/8] Tiny tidy up --- src/ClassRegistryInitExtension.php | 2 ++ src/LoadComponentOnFlyMethodReturnTypeExtension.php | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/ClassRegistryInitExtension.php b/src/ClassRegistryInitExtension.php index 612e765..e81f48b 100644 --- a/src/ClassRegistryInitExtension.php +++ b/src/ClassRegistryInitExtension.php @@ -5,6 +5,7 @@ namespace ARiddlestone\PHPStanCakePHP2; use ARiddlestone\PHPStanCakePHP2\Service\SchemaService; +use Exception; use Inflector; use PhpParser\ConstExprEvaluationException; use PhpParser\ConstExprEvaluator; @@ -48,6 +49,7 @@ public function isStaticMethodSupported( /** * @throws ShouldNotHappenException * @throws ConstExprEvaluationException + * @throws Exception */ public function getTypeFromStaticMethodCall( MethodReflection $methodReflection, diff --git a/src/LoadComponentOnFlyMethodReturnTypeExtension.php b/src/LoadComponentOnFlyMethodReturnTypeExtension.php index c0c6d63..900a01c 100644 --- a/src/LoadComponentOnFlyMethodReturnTypeExtension.php +++ b/src/LoadComponentOnFlyMethodReturnTypeExtension.php @@ -5,6 +5,7 @@ namespace ARiddlestone\PHPStanCakePHP2; use Component; +use ComponentCollection; use PhpParser\Node\Expr\MethodCall; use PhpParser\Node\Scalar\String_; use PHPStan\Analyser\Scope; @@ -25,7 +26,7 @@ public function __construct(ReflectionProvider $reflectionProvider) public function getClass(): string { - return \ComponentCollection::class; + return ComponentCollection::class; } public function isMethodSupported(MethodReflection $methodReflection): bool From da6115bc5f7a4475a047377040bda84060298655 Mon Sep 17 00:00:00 2001 From: Andrew Riddlestone Date: Mon, 6 Apr 2026 16:58:21 +0100 Subject: [PATCH 7/8] Attempting to fix mermaid hyperlink --- docs/README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/README.md b/docs/README.md index 891e53a..fe35286 100644 --- a/docs/README.md +++ b/docs/README.md @@ -71,9 +71,9 @@ classDiagram AppHelper --|> Helper Helper --> "*" Helper - click Component href "Components.md" "See PHPStan customizations for Components" - click Controller href "Controllers.md" "See PHPStan customizations for Controllers" - click Helper href "Helpers.md" "See PHPStan customizations for Helpers" - click Model href "Models.md" "See PHPStan customizations for Models" - click Shell href "Shells.md" "See PHPStan customizations for Shells" + click Component href "./Components.md" "See PHPStan customizations for Components" + click Controller href "./Controllers.md" "See PHPStan customizations for Controllers" + click Helper href "./Helpers.md" "See PHPStan customizations for Helpers" + click Model href "./Models.md" "See PHPStan customizations for Models" + click Shell href "./Shells.md" "See PHPStan customizations for Shells" ``` From 4a467d99798b00c3139abc9e41228fd0806d3b33 Mon Sep 17 00:00:00 2001 From: Andrew Riddlestone Date: Mon, 6 Apr 2026 17:01:37 +0100 Subject: [PATCH 8/8] Removed mermaid hyperlinks --- docs/README.md | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/docs/README.md b/docs/README.md index fe35286..403407a 100644 --- a/docs/README.md +++ b/docs/README.md @@ -3,7 +3,7 @@ ## MVC Extensions PHPStan extensions are available for several MVC classes in CakePHP to help PHPStan understand the otherwise -undocumented methods and properties of these classes. +undocumented methods and properties of these classes. The links covered are shown in the diagram below. ```mermaid classDiagram @@ -70,10 +70,4 @@ classDiagram AppHelper --|> Helper Helper --> "*" Helper - - click Component href "./Components.md" "See PHPStan customizations for Components" - click Controller href "./Controllers.md" "See PHPStan customizations for Controllers" - click Helper href "./Helpers.md" "See PHPStan customizations for Helpers" - click Model href "./Models.md" "See PHPStan customizations for Models" - click Shell href "./Shells.md" "See PHPStan customizations for Shells" ```