Skip to content

Commit

Permalink
Local extensions called in static "extends" fire after construct
Browse files Browse the repository at this point in the history
Local extensions, by design, are made against a fully constructed extendable class. However, previously, if called within the scope of a static `extends` call (which is effectively a constructor extension), it was firing immediately which meant it fired before behaviors were loaded.

This will store any callbacks for local extensions fired in a constructor extension until after the construction is completed.
  • Loading branch information
bennothommo committed Jun 21, 2023
1 parent facb27d commit a8ba4ee
Show file tree
Hide file tree
Showing 2 changed files with 45 additions and 0 deletions.
22 changes: 22 additions & 0 deletions src/Extension/Extendable.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,29 @@ class Extendable
*/
public $implement = null;

/**
* Indicates if the extendable constructor has completed.
*/
protected bool $extendableConstructed = false;

/**
* This stores any locally-scoped callbacks fired before the extendable constructor had completed.
*/
protected array $localCallbacks = [];

/**
* Constructor
*/
public function __construct()
{
$this->extendableConstruct();
$this->extendableConstructed = true;

if (count($this->localCallbacks)) {
foreach ($this->localCallbacks as $callback) {
$this->extendableAddLocalExtension($callback[0], $callback[1]);
}
}
}

public function __get($name)
Expand Down Expand Up @@ -98,6 +115,11 @@ public static function extendableAddExtension(callable $callback, bool $scoped =
*/
protected function extendableAddLocalExtension(\Closure $callback, ?object $outerScope = null)
{
if (!$this->extendableConstructed) {
$this->localCallbacks[] = [$callback, $outerScope];
return;
}

return $callback->call($this, $outerScope ?? $this);
}
}
23 changes: 23 additions & 0 deletions tests/Extension/ExtendableTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,29 @@ public function testLocalExtensionCreatingDynamicMethodWithinScope()
// Should not contain the same dynamic method as it's declared locally
$this->assertFalse($object->methodExists('changeFoo'));
}

/**
* This tests using a local extension to call a method from a behavior within a scoped extension on the constructor.
* It more or less tests the initialisation order of extensions, ie:
*
* - Scoped and constructor extensions are fired first
* - Behaviours are then loaded
* - Local extensions are then fired, if called within the construction process, or on request within the run-time.
*/
public function testLocalExtensionWithinScopedExtension()
{
$result = null;

ExtendableTestExampleExtendableClassDotNotation::extend(function () use (&$result) {
$this->extend(function () use (&$result) {
$result = $this->getFoo();
});
}, true);

$class = new ExtendableTestExampleExtendableClassDotNotation;

$this->assertEquals('foo', $result);
}
}

//
Expand Down

0 comments on commit a8ba4ee

Please sign in to comment.