Skip to content

Commit 37a06ae

Browse files
authored
Merge pull request #29 from CakeDC/issue/cakephp-rule-accept-set-classname
Issue/cakephp rule accept set classname
2 parents 102f5cc + 2f0ff52 commit 37a06ae

File tree

6 files changed

+132
-4
lines changed

6 files changed

+132
-4
lines changed

rules.neon

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ parametersSchema:
1616
])
1717

1818
conditionalTags:
19+
CakeDC\PHPStan\Visitor\AddAssociationSetClassNameVisitor:
20+
phpstan.parser.richParserNodeVisitor: %cakeDC.addAssociationExistsTableClassRule%
1921
CakeDC\PHPStan\Rule\Model\AddAssociationExistsTableClassRule:
2022
phpstan.rules.rule: %cakeDC.addAssociationExistsTableClassRule%
2123
CakeDC\PHPStan\Rule\Model\AddAssociationMatchOptionsTypesRule:
@@ -28,6 +30,8 @@ conditionalTags:
2830
phpstan.rules.rule: %cakeDC.ormSelectQueryFindMatchOptionsTypesRule%
2931

3032
services:
33+
-
34+
class: CakeDC\PHPStan\Visitor\AddAssociationSetClassNameVisitor
3135
-
3236
class: CakeDC\PHPStan\Rule\Model\AddAssociationExistsTableClassRule
3337
-

src/Rule/LoadObjectExistsCakeClassRule.php

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,10 +66,13 @@ public function processNode(Node $node, Scope $scope): array
6666
return [];
6767
}
6868

69-
$inputClassName = $this->getInputClassName(
70-
$details['alias']->value,
71-
$details['options']
72-
);
69+
$inputClassName = $this->getInputClassNameFromNode($node);
70+
if ($inputClassName === null) {
71+
$inputClassName = $this->getInputClassName(
72+
$details['alias']->value,
73+
$details['options']
74+
);
75+
}
7376
if ($this->getTargetClassName($inputClassName)) {
7477
return [];
7578
}
@@ -130,4 +133,13 @@ abstract protected function getTargetClassName(string $name): ?string;
130133
* @return array{'alias': ?\PhpParser\Node\Arg, 'options': ?\PhpParser\Node\Arg, 'sourceMethods':array<string>}|null
131134
*/
132135
abstract protected function getDetails(string $reference, array $args): ?array;
136+
137+
/**
138+
* @param \PhpParser\Node\Expr\MethodCall $node
139+
* @return string|null
140+
*/
141+
protected function getInputClassNameFromNode(MethodCall $node): ?string
142+
{
143+
return null;
144+
}
133145
}

src/Rule/Model/AddAssociationExistsTableClassRule.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@
1616
use Cake\ORM\AssociationCollection;
1717
use CakeDC\PHPStan\Rule\LoadObjectExistsCakeClassRule;
1818
use CakeDC\PHPStan\Utility\CakeNameRegistry;
19+
use CakeDC\PHPStan\Visitor\AddAssociationSetClassNameVisitor;
20+
use PhpParser\Node\Expr;
21+
use PhpParser\Node\Expr\MethodCall;
1922

2023
class AddAssociationExistsTableClassRule extends LoadObjectExistsCakeClassRule
2124
{
@@ -69,4 +72,17 @@ protected function getDetails(string $reference, array $args): ?array
6972

7073
return null;
7174
}
75+
76+
/**
77+
* @inheritDoc
78+
*/
79+
protected function getInputClassNameFromNode(MethodCall $node): ?string
80+
{
81+
$setClassNameValue = $node->getAttribute(AddAssociationSetClassNameVisitor::ATTRIBUTE_NAME);
82+
if ($setClassNameValue instanceof Expr) {
83+
return $this->parseClassNameFromExprTrait($setClassNameValue);
84+
}
85+
86+
return null;
87+
}
7288
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace CakeDC\PHPStan\Visitor;
5+
6+
use CakeDC\PHPStan\Rule\Traits\ParseClassNameFromArgTrait;
7+
use PhpParser\Node;
8+
use PhpParser\Node\Expr;
9+
use PhpParser\Node\Expr\MethodCall;
10+
use PhpParser\NodeVisitorAbstract;
11+
12+
class AddAssociationSetClassNameVisitor extends NodeVisitorAbstract
13+
{
14+
use ParseClassNameFromArgTrait;
15+
16+
public const ATTRIBUTE_NAME = 'addAssociationVisitorOptions';
17+
18+
/**
19+
* @param \PhpParser\Node\Expr|null $optionsSet
20+
*/
21+
public function __construct(protected ?Expr $optionsSet = null)
22+
{
23+
}
24+
25+
/**
26+
* @param array<\PhpParser\Node> $nodes
27+
* @return array<\PhpParser\Node>|null
28+
*/
29+
public function beforeTraverse(array $nodes): ?array
30+
{
31+
$this->optionsSet = null;
32+
33+
return null;
34+
}
35+
36+
/**
37+
* @param \PhpParser\Node $node
38+
* @return \PhpParser\Node|null
39+
*/
40+
public function enterNode(Node $node): ?Node
41+
{
42+
if (!$node instanceof MethodCall || !$node->name instanceof Node\Identifier) {
43+
return null;
44+
}
45+
if ($this->optionsSet === null && $node->name->name === 'setClassName') {
46+
$this->optionsSet = $node->args[0]->value ?? null;
47+
}
48+
if (in_array($node->name->name, ['load', 'belongsTo', 'belongsToMany', 'hasOne', 'hasMany'])) {
49+
$node->setAttribute(self::ATTRIBUTE_NAME, $this->optionsSet);
50+
}
51+
52+
return null;
53+
}
54+
55+
/**
56+
* @inheritDoc
57+
*/
58+
public function leaveNode(Node $node)
59+
{
60+
$this->optionsSet = null;
61+
62+
return null;
63+
}
64+
}

tests/TestCase/Rule/Model/AddAssociationExistsTableClassRuleTest.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,22 @@ public function testRule(): void
5454
'Call to Cake\ORM\AssociationCollection::load could not find the class for "PalUsers"',
5555
148,
5656
],
57+
[
58+
'Call to CakeDC\PHPStan\Test\TestCase\Rule\Model\Fake\FailingRuleItemsTable::hasOne could not find the class for "Articles"',
59+
187,
60+
],
61+
[
62+
'Call to CakeDC\PHPStan\Test\TestCase\Rule\Model\Fake\FailingRuleItemsTable::hasOne could not find the class for "SomeArticles"',
63+
191,
64+
],
5765
]);
5866
}
67+
68+
/**
69+
* @inheritDoc
70+
*/
71+
public static function getAdditionalConfigFiles(): array
72+
{
73+
return [__DIR__ . '/../../../../extension.neon'];
74+
}
5975
}

tests/TestCase/Rule/Model/Fake/FailingRuleItemsTable.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,5 +179,21 @@ public function initialize(array $config): void
179179
'targetTable' => TableRegistry::getTableLocator()->get('Articles'),
180180
]);
181181
$this->associations()->load(HasOne::class, 'Notes');
182+
$this->hasOne('BakedArticles', [
183+
'cascadeCallbacks' => true,
184+
'conditions' => ['BakedArticles.baked' => 1],
185+
])->setClassName(VeryCustomize00009ArticlesTable::class);
186+
187+
$this->hasOne('CakeArticles', [
188+
'cascadeCallbacks' => true,
189+
'conditions' => ['CakeArticles.category_id' => 10],
190+
])->setClassName('Articles');
191+
$this->hasOne('CakeArticles')
192+
->setFinder('myFinder')
193+
->setClassName('SomeArticles');
194+
$this->associations()->load(HasOne::class, 'CleanArticles', [
195+
'cascadeCallbacks' => true,
196+
'conditions' => ['CleanArticles.clean' => 1],
197+
])->setClassName(VeryCustomize00009ArticlesTable::class);
182198
}
183199
}

0 commit comments

Comments
 (0)