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
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:
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.
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
Here a more complex using static counter:
Demo: https://3v4l.org/smcDi