Custom PHPStan rul lto allow calling class method only from specific class

31 Views Asked by At

I'm trying to write a custom phpstan rule to not allow other devs to call Cart::save() ($cart->save()) from everywhere in the codebase, but to call only from CartRepository, and if it's called outside of repository I want to see an error like Calling Cart::save() method outside of CartRepository class is not allowed.

I tried to do something like this

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');
    }
}

but it's not working as I expect, let's say in Cart I have a method :

    private function updateVariant()
    {
        $variant =  Variant::
            ->where('id', '=', $this->variantId)
            ->first();
        
        // logic to update variant        

        $variant->save();
    }

and my rule will scream on line $variant->save(); because save is called by Cart, but it's not Cart related save

1

There are 1 best solutions below

2
delboy1978uk On

Why not just override the save() method of the cart instead? Something like this:

<?php

class Cart
{
    public function save(object $repo = null)
    {
        if ($repo === null || !$repo instanceof CartRepository) {
            throw new Exception('Save the cart using the CartRepository' . "\n");
        }
        
        $repo->save($this);
    }
}

class CartRepository
{
    public function save(Cart $cart)
    {
        return 'saved!';
    }
}

$repo = new CartRepository();
$cart = new Cart();

try {
    $cart->save();
} catch (Exception $e) {
    echo $e->getMessage();
}

echo $repo->save($cart);

This will output:

Save the cart using the CartRepository

saved!

You can try it here https://3v4l.org/uHW1e