编写PHPStan自定义规则限制Cart::save()仅从CartRepository调用
问题:PHPStan自定义规则误判非Cart类的save()调用
需求概述
禁止在代码库任意位置调用Cart::save()(即$cart->save()),仅允许从CartRepository类调用该方法;外部调用时抛出错误:"Calling Cart::save() method outside of CartRepository class is not allowed."
现有规则问题
当前规则会误判:在Cart类的updateVariant方法中调用$variant->save()(Variant类的save方法)时,也触发错误。原因是规则仅检查调用发起类为Cart,未验证被调用的对象是否是Cart实例,导致所有在Cart类内调用的save()方法都被误判。
现有规则代码
class CartSaveRule implements Rule { public function getNodeType(): string { return MethodCall::class; } /** * @param MethodCall $node * @param Scope $scope * @return array */ public function processNode(Node $node, Scope $scope): array { // Check if the method call is to the "save" method if ($node->name->toString() === 'save') { $className = $scope->getClassReflection()->getName(); // Check if the method call is made from the Product class if ($className === 'Cart') { // Get the method call location $line = $node->getLine(); $file = $scope->getFile(); // Check if the method call is not made from inside the ProductRepository class if (!$this->isCalledFromProductRepository($scope)) { return [ RuleErrorBuilder::message('Calling save() method of Cart class outside of CartRepository class is not allowed.') ->line($line) ->file($file) ->build() ]; } } } return []; } private function isCalledFromRepository(Scope $scope): bool { // Check if the calling class is CartRepository or its subclass return $scope->getClassReflection()->isSubclassOf('CartRepository'); } }
问题分析
- 对象类型判断缺失:仅检查调用发起类,未验证被调用对象是否为
Cart实例,导致误判其他类的save()调用。 - 方法名笔误:
isCalledFromProductRepository方法未定义,实际调用的是isCalledFromRepository,逻辑不一致。 - 逻辑颠倒:错误地将调用发起类为
Cart作为触发条件,而非针对Cart实例的save()调用。
修正后的规则代码
use PhpParser\Node; use PhpParser\Node\Expr\MethodCall; use PHPStan\Analyser\Scope; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; class CartSaveRule implements Rule { private const ALLOWED_CLASS = 'CartRepository'; private const TARGET_CLASS = 'Cart'; private const ERROR_MESSAGE = 'Calling Cart::save() method outside of CartRepository class is not allowed.'; public function getNodeType(): string { return MethodCall::class; } /** * @param MethodCall $node * @param Scope $scope * @return array<int, \PHPStan\Rules\RuleError> */ public function processNode(Node $node, Scope $scope): array { // 1. 过滤非save方法的调用 if ($node->name->toString() !== 'save') { return []; } // 2. 验证被调用对象是否为Cart类实例 $calledType = $scope->getType($node->var); $calledClass = $calledType->getClassReflection(); if ($calledClass === null || $calledClass->getName() !== self::TARGET_CLASS) { return []; } // 3. 检查调用上下文是否为允许的类 $currentClass = $scope->getClassReflection(); if ($currentClass === null) { // 全局代码中调用,直接报错 return $this->createError($node, $scope); } if (!$currentClass->isSubclassOf(self::ALLOWED_CLASS) && $currentClass->getName() !== self::ALLOWED_CLASS) { // 非CartRepository及其子类中调用,报错 return $this->createError($node, $scope); } return []; } /** * 生成错误信息 */ private function createError(MethodCall $node, Scope $scope): array { return [ RuleErrorBuilder::message(self::ERROR_MESSAGE) ->line($node->getLine()) ->file($scope->getFile()) ->build() ]; } }
修正说明
- 精准匹配对象类型:通过
$scope->getType($node->var)获取被调用对象的类型,仅针对Cart实例的save()方法触发检查。 - 完善上下文验证:检查调用所在类是否为
CartRepository或其子类,同时处理全局代码中的调用场景。 - 代码优化:用常量固定配置值,拆分错误生成逻辑,提升代码可读性和维护性。
内容的提问来源于stack exchange,提问作者Bogdan Dubyk
相关产品推荐
相关产品推荐

