skip to Main Content

Description

I’ve implemented a Strategy class representing various strategies. Each strategy, upon creation, accepts a function as a parameter. This function is then bound to the strategy, allowing the use of $this within the anonymous function. Here’s an example:

$a = new Strategy(function () {
    $this->getName(); // Strategy 1. $this refers to $a within this function.
});

Within the constructor of the Strategy class:

public function __construct(Closure $strategy)
{
    self::initCounter();
    $this->strategy = $strategy(...)->bindTo($this);

    $this->name = 'Strategy ' . self::$strategyCounter->getNextStrategyNumber();
}

The initCounter method initializes counter which is simple private static field with object of StrategyCounter class which have only number of Strategies and method to get next number – getNextNumber() and to decrement number – decrementStrategyCounter()

The principle of operation is simple:

When creating a new Strategy, it receives the default name: "Strategy X", where X is the number of the next strategy. For example:

$a = new Strategy(function () {}); // Name = Strategy 1
$b = new Strategy(function () {}); // Name = Strategy 2
// and so on.

Where is the problem and what I want to achive?

I also wanted the name of the Strategy to not be blocked, i.e. if the object is deleted/unset, the counter should be decremented so that the name is available for the new strategy like this:

$a = new Strategy(function () {}); // Name = Strategy 1
$b = new Strategy(function () {}); // Name = Strategy 2
unset($b); // $b is unset => Counter decremented => Default name for next Strategy should be with number 2.
$c = new Strategy(function () {}); // Name = Strategy 2

Unfortunately, I am not able to achieve this using __destruct() because the reference to the Strategy is in Closure

What I tried

I thought maybe I should remove Closure first in the destructor, but that didn’t work:

public function __destruct()
{
    unset($this->strategy);
    self::$strategyCounter->decrementStrategyCounter();
}

This is because the destructor is not called at all.
I understand that like this:

$a = new Strategy(function () {}); // Name = Strategy 1
$b = new Strategy(function () {}); // Name = Strategy 2
unset($b); // $b isn't unset, because Strategy still exists, because was bind to Closure within that Strategy, and since this is the case, no destuctor is called.
$c = new Strategy(function () {}); // Name = Strategy 3 Blehh ;C

// The script ends, all Strategies are removed one by one.

2

Answers


  1. As you’ve worked out, the problem is that you have a circular reference – the strategy references the closure, and the closure references the strategy.

    The destructor will eventually be called when PHP checks for orphaned circular references. If there’s a specific place in your code where you unset strategies, you could call gc_collect_cycles after doing so to have them immediately destroyed.

    Alternatively, you need to change the design to avoid circular references. For instance, rather than binding the closure to the Strategy object, pass the Strategy in as a parameter each time you invoke it:

    // Inside Strategy class
    public function invoke() {
        ($this->strategy)($this);
    }
    
    // Outside
    $a = new Strategy(function (Strategy $s) {
        $strat->getName(); // Strategy 1. $strat will be passed the object $a
    });
    

    If you already pass parameters to the callback in the real code, just add the Strategy before them – conceptually, $this can be seen as just a parameter with special syntax.

    Login or Signup to reply.
  2. You can barely have a simple Interface which expects a run() method to exist. From that you can call it with the $this context. Each $strategy instance has it’s own $this scope.

    Demo: https://3v4l.org/eCtZU

    interface StrategyInterface {
        public function run();
    }
    
    class Strategy implements StrategyInterface
    {
        protected ?string $value = null;
        public function __construct(private Closure $closure)
        {
            $this->value = (string) rand(1, 100);
        }
    
        public function run()
        {
            $this->closure->call($this);
        }
    }
    
    for ($i = 0; $i < 10; ++$i) {
        $strategy = new Strategy(function() {
            var_dump($this->value);
        });
    
        $strategy->run();
    }
    
    

    Here a more complex using static counter:

    Demo: https://3v4l.org/smcDi

    interface StrategyInterface
    {
        public function run();
    }
    
    class Strategy implements StrategyInterface
    {
        protected ?string $value = null;
        private static $counter = 0;
    
        public function __construct(private Closure $closure)
        {
            echo "Strategy Counter: ", ++self::$counter, PHP_EOL;
            $this->value = (string)rand(1, 100);
        }
    
        public function run()
        {
            $this->closure->call($this);
        }
    
        public function __destruct()
        {
            echo "Strategy Counter: ", --self::$counter, PHP_EOL;
        }
    }
    
    $strategy1 = new Strategy(function () {
        var_dump($this->value);
    });
    
    $strategy2 = new Strategy(function () {
        var_dump($this->value);
    });
    
    $strategy3 = new Strategy(function () {
        var_dump($this->value);
    });
    
    
    $strategy1->run(); // counter still 3, because 3 are created
    $strategy1 = null; // destroy the first strategy, counter is 2
    $strategy2->run();
    unset($strategy2); // destroy the second strategy, counter is 1
    $strategy3->run();
    
    $strategy4 = new Strategy(function () { // create a new strategy, counter is 2
        var_dump($this->value);
    });
    
    // program ends, counter goes down to 0
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search