Skip to content

Commit e80d68c

Browse files
author
Timm Ortloff
committed
Rebind Attribute callbacks when cloning also add rebind() and rebindCallbacks() to Attributes
1 parent 11436c2 commit e80d68c

File tree

2 files changed

+81
-14
lines changed

2 files changed

+81
-14
lines changed

src/Attributes.php

+60-14
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@
44

55
use ArrayAccess;
66
use ArrayIterator;
7+
use Closure;
78
use InvalidArgumentException;
89
use IteratorAggregate;
10+
use ReflectionFunction;
911
use Traversable;
1012

1113
use function ipl\Stdlib\get_php_type;
@@ -365,29 +367,19 @@ public function setPrefix($prefix)
365367
/**
366368
* Register callback for an attribute
367369
*
368-
* @param string $name Name of the attribute to register the callback for
369-
* @param callable $callback Callback to call when retrieving the attribute
370-
* @param callable $setterCallback Callback to call when setting the attribute
370+
* @param string $name Name of the attribute to register the callback for
371+
* @param ?callable $callback Callback to call when retrieving the attribute
372+
* @param ?callable $setterCallback Callback to call when setting the attribute
371373
*
372374
* @return $this
373-
*
374-
* @throws InvalidArgumentException If $callback is not callable or if $setterCallback is set and not callable
375375
*/
376-
public function registerAttributeCallback($name, $callback, $setterCallback = null)
376+
public function registerAttributeCallback(string $name, ?callable $callback, ?callable $setterCallback = null): self
377377
{
378378
if ($callback !== null) {
379-
if (! is_callable($callback)) {
380-
throw new InvalidArgumentException(__METHOD__ . ' expects a callable callback');
381-
}
382-
383379
$this->callbacks[$name] = $callback;
384380
}
385381

386382
if ($setterCallback !== null) {
387-
if (! is_callable($setterCallback)) {
388-
throw new InvalidArgumentException(__METHOD__ . ' expects a callable setterCallback');
389-
}
390-
391383
$this->setterCallbacks[$name] = $setterCallback;
392384
}
393385

@@ -518,4 +510,58 @@ public function getIterator(): Traversable
518510
{
519511
return new ArrayIterator($this->attributes);
520512
}
513+
514+
/**
515+
* Rebind all callbacks that point to `$oldThisId` to `$newThis`
516+
*
517+
* @param int $oldThisId
518+
* @param object $newThis
519+
*/
520+
public function rebind(int $oldThisId, object $newThis): void
521+
{
522+
$this->rebindCallbacks($this->callbacks, $oldThisId, $newThis);
523+
$this->rebindCallbacks($this->setterCallbacks, $oldThisId, $newThis);
524+
}
525+
526+
/**
527+
* Loops over all `$callbacks`, binds them to `$newThis` only where `$oldThisId` matches. The callbacks are
528+
* modified directly on the `$callbacks` reference.
529+
*
530+
* @param callable[] $callbacks
531+
* @param int $oldThisId
532+
* @param object $newThis
533+
*/
534+
private function rebindCallbacks(array &$callbacks, int $oldThisId, object $newThis): void
535+
{
536+
foreach ($callbacks as &$callback) {
537+
if (! $callback instanceof Closure) {
538+
if (is_array($callback) && ! is_string($callback[0])) {
539+
if (spl_object_id($callback[0]) === $oldThisId) {
540+
$callback[0] = $newThis;
541+
}
542+
}
543+
544+
continue;
545+
}
546+
547+
$closureThis = (new ReflectionFunction($callback))
548+
->getClosureThis();
549+
550+
// Closure is most likely static
551+
if ($closureThis === null) {
552+
continue;
553+
}
554+
555+
if (spl_object_id($closureThis) === $oldThisId) {
556+
$callback = $callback->bindTo($newThis);
557+
}
558+
}
559+
}
560+
561+
public function __clone()
562+
{
563+
foreach ($this->attributes as &$attribute) {
564+
$attribute = clone $attribute;
565+
}
566+
}
521567
}

src/BaseHtmlElement.php

+21
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,9 @@ abstract class BaseHtmlElement extends HtmlDocument
7575
/** @var string Tag of element. Set this property in order to provide the element's tag when extending this class */
7676
protected $tag;
7777

78+
/** @var int Holds an ID to identify itself, used to get the ID of the Object for comparison when cloning */
79+
private $thisRefId;
80+
7881
/**
7982
* Get the attributes of the element
8083
*
@@ -83,6 +86,8 @@ abstract class BaseHtmlElement extends HtmlDocument
8386
public function getAttributes()
8487
{
8588
if ($this->attributes === null) {
89+
$this->thisRefId = spl_object_id($this);
90+
8691
$default = $this->getDefaultAttributes();
8792
if (empty($default)) {
8893
$this->attributes = new Attributes();
@@ -105,6 +110,8 @@ public function getAttributes()
105110
*/
106111
public function setAttributes($attributes)
107112
{
113+
$this->thisRefId = spl_object_id($this);
114+
108115
$this->attributes = Attributes::wantAttributes($attributes);
109116

110117
$this->attributeCallbacksRegistered = false;
@@ -352,4 +359,18 @@ public function renderUnwrapped()
352359
$tag
353360
);
354361
}
362+
363+
public function __clone()
364+
{
365+
parent::__clone();
366+
367+
if ($this->attributes !== null) {
368+
$this->attributes = clone $this->attributes;
369+
370+
// `$this->thisRefId` is the ID to this Object prior of cloning, `$this` is the newly cloned Object
371+
$this->attributes->rebind($this->thisRefId, $this);
372+
373+
$this->thisRefId = spl_object_id($this);
374+
}
375+
}
355376
}

0 commit comments

Comments
 (0)