Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down Expand Up @@ -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"
}
}
}
}
11 changes: 11 additions & 0 deletions docs/Components.md
Original file line number Diff line number Diff line change
@@ -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`.
31 changes: 31 additions & 0 deletions docs/Controllers.md
Original file line number Diff line number Diff line change
@@ -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.
11 changes: 11 additions & 0 deletions docs/Helpers.md
Original file line number Diff line number Diff line change
@@ -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`.
75 changes: 75 additions & 0 deletions docs/Models.md
Original file line number Diff line number Diff line change
@@ -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
73 changes: 73 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# 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. The links covered are shown in the diagram below.

```mermaid
classDiagram
namespace CakePHP {
class Shell {
<<abstract>>
string[] tasks
string[] uses
}
class Model {
<<abstract>>
string[] actsAs
array hasOne
array hasMany
array belongsTo
array hasAndBelongsToMany
array findMethods
array mapMethods
}
class Behavior {
<<abstract>>
}
class Controller {
<<abstract>>
string[] components
string[] helpers
string[] uses
}
class Component {
<<abstract>>
string[] components
}
class Helper {
<<abstract>>
string[] helpers
}
}
namespace App {
class AppShell {
<<abstract>>
}
class AppModel {
<<abstract>>
}
class AppController {
<<abstract>>
}
class AppHelper {
<<abstract>>
}
}
AppShell --|> Shell
Shell --> "*" Shell
Shell --> "*" Model

AppModel --|> Model
Model --> "*" Behavior

AppController --|> Controller
Controller --> "*" Model
Controller --> "*" Component
Controller --> "*" Helper
Component --> "*" Component

AppHelper --|> Helper
Helper --> "*" Helper
```
22 changes: 22 additions & 0 deletions docs/Shells.md
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions phpinsights.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@

'exclude' => [
'phpstan',
'stubs',
'tests',
'vendor',
],
Expand Down
5 changes: 4 additions & 1 deletion src/ClassReflectionFinder.php
Original file line number Diff line number Diff line change
Expand Up @@ -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']
Expand Down
36 changes: 27 additions & 9 deletions src/ClassRegistryInitExtension.php
Original file line number Diff line number Diff line change
@@ -1,31 +1,36 @@
<?php

declare(strict_types=1);

namespace ARiddlestone\PHPStanCakePHP2;

use ARiddlestone\PHPStanCakePHP2\Service\SchemaService;
use Exception;
use Inflector;
use PhpParser\ConstExprEvaluationException;
use PhpParser\ConstExprEvaluator;
use PhpParser\Node\Expr\StaticCall;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Reflection\ReflectionProvider;
use PHPStan\ShouldNotHappenException;
use PHPStan\Type\BooleanType;
use PHPStan\Type\DynamicStaticMethodReturnTypeExtension;
use PHPStan\Type\DynamicStaticMethodReturnTypeExtension as ReturnTypeExtension;
use PHPStan\Type\ObjectType;
use PHPStan\Type\ObjectWithoutClassType;
use PHPStan\Type\Type;
use PHPStan\Type\UnionType;

class ClassRegistryInitExtension implements DynamicStaticMethodReturnTypeExtension
final class ClassRegistryInitExtension implements ReturnTypeExtension
{
private ReflectionProvider $reflectionProvider;

private SchemaService $schemaService;

public function __construct(
ReflectionProvider $reflectionProvider,
SchemaService $schemaService)
{
SchemaService $schemaService
) {
$this->reflectionProvider = $reflectionProvider;
$this->schemaService = $schemaService;
}
Expand All @@ -35,13 +40,22 @@ 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
* @throws Exception
*/
public function getTypeFromStaticMethodCall(
MethodReflection $methodReflection,
StaticCall $methodCall,
Scope $scope
): Type {
$arg1 = $methodCall->getArgs()[0]->value;
$evaluator = new ConstExprEvaluator();
$arg1 = $evaluator->evaluateSilently($arg1);
Expand All @@ -54,14 +68,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(),
]);
}
}
Loading
Loading