Skip to content

Commit

Permalink
Rebind Attribute callbacks when cloning also add rebind() and `rebi…
Browse files Browse the repository at this point in the history
…ndCallbacks()` to `Attributes`
  • Loading branch information
Timm Ortloff committed Feb 2, 2023
1 parent 11436c2 commit e80d68c
Show file tree
Hide file tree
Showing 2 changed files with 81 additions and 14 deletions.
74 changes: 60 additions & 14 deletions src/Attributes.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@

use ArrayAccess;
use ArrayIterator;
use Closure;
use InvalidArgumentException;
use IteratorAggregate;
use ReflectionFunction;
use Traversable;

use function ipl\Stdlib\get_php_type;
Expand Down Expand Up @@ -365,29 +367,19 @@ public function setPrefix($prefix)
/**
* Register callback for an attribute
*
* @param string $name Name of the attribute to register the callback for
* @param callable $callback Callback to call when retrieving the attribute
* @param callable $setterCallback Callback to call when setting the attribute
* @param string $name Name of the attribute to register the callback for
* @param ?callable $callback Callback to call when retrieving the attribute
* @param ?callable $setterCallback Callback to call when setting the attribute
*
* @return $this
*
* @throws InvalidArgumentException If $callback is not callable or if $setterCallback is set and not callable
*/
public function registerAttributeCallback($name, $callback, $setterCallback = null)
public function registerAttributeCallback(string $name, ?callable $callback, ?callable $setterCallback = null): self
{
if ($callback !== null) {
if (! is_callable($callback)) {
throw new InvalidArgumentException(__METHOD__ . ' expects a callable callback');
}

$this->callbacks[$name] = $callback;
}

if ($setterCallback !== null) {
if (! is_callable($setterCallback)) {
throw new InvalidArgumentException(__METHOD__ . ' expects a callable setterCallback');
}

$this->setterCallbacks[$name] = $setterCallback;
}

Expand Down Expand Up @@ -518,4 +510,58 @@ public function getIterator(): Traversable
{
return new ArrayIterator($this->attributes);
}

/**
* Rebind all callbacks that point to `$oldThisId` to `$newThis`
*
* @param int $oldThisId
* @param object $newThis
*/
public function rebind(int $oldThisId, object $newThis): void
{
$this->rebindCallbacks($this->callbacks, $oldThisId, $newThis);
$this->rebindCallbacks($this->setterCallbacks, $oldThisId, $newThis);
}

/**
* Loops over all `$callbacks`, binds them to `$newThis` only where `$oldThisId` matches. The callbacks are
* modified directly on the `$callbacks` reference.
*
* @param callable[] $callbacks
* @param int $oldThisId
* @param object $newThis
*/
private function rebindCallbacks(array &$callbacks, int $oldThisId, object $newThis): void
{
foreach ($callbacks as &$callback) {
if (! $callback instanceof Closure) {
if (is_array($callback) && ! is_string($callback[0])) {
if (spl_object_id($callback[0]) === $oldThisId) {
$callback[0] = $newThis;
}
}

continue;
}

$closureThis = (new ReflectionFunction($callback))
->getClosureThis();

// Closure is most likely static
if ($closureThis === null) {
continue;
}

if (spl_object_id($closureThis) === $oldThisId) {
$callback = $callback->bindTo($newThis);
}
}
}

public function __clone()
{
foreach ($this->attributes as &$attribute) {
$attribute = clone $attribute;
}
}
}
21 changes: 21 additions & 0 deletions src/BaseHtmlElement.php
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@ abstract class BaseHtmlElement extends HtmlDocument
/** @var string Tag of element. Set this property in order to provide the element's tag when extending this class */
protected $tag;

/** @var int Holds an ID to identify itself, used to get the ID of the Object for comparison when cloning */
private $thisRefId;

/**
* Get the attributes of the element
*
Expand All @@ -83,6 +86,8 @@ abstract class BaseHtmlElement extends HtmlDocument
public function getAttributes()
{
if ($this->attributes === null) {
$this->thisRefId = spl_object_id($this);

$default = $this->getDefaultAttributes();
if (empty($default)) {
$this->attributes = new Attributes();
Expand All @@ -105,6 +110,8 @@ public function getAttributes()
*/
public function setAttributes($attributes)
{
$this->thisRefId = spl_object_id($this);

$this->attributes = Attributes::wantAttributes($attributes);

$this->attributeCallbacksRegistered = false;
Expand Down Expand Up @@ -352,4 +359,18 @@ public function renderUnwrapped()
$tag
);
}

public function __clone()
{
parent::__clone();

if ($this->attributes !== null) {
$this->attributes = clone $this->attributes;

// `$this->thisRefId` is the ID to this Object prior of cloning, `$this` is the newly cloned Object
$this->attributes->rebind($this->thisRefId, $this);

$this->thisRefId = spl_object_id($this);
}
}
}

0 comments on commit e80d68c

Please sign in to comment.