|
4 | 4 |
|
5 | 5 | use ArrayAccess;
|
6 | 6 | use ArrayIterator;
|
| 7 | +use Closure; |
7 | 8 | use InvalidArgumentException;
|
8 | 9 | use IteratorAggregate;
|
| 10 | +use ReflectionFunction; |
9 | 11 | use Traversable;
|
10 | 12 |
|
11 | 13 | use function ipl\Stdlib\get_php_type;
|
@@ -365,29 +367,19 @@ public function setPrefix($prefix)
|
365 | 367 | /**
|
366 | 368 | * Register callback for an attribute
|
367 | 369 | *
|
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 |
371 | 373 | *
|
372 | 374 | * @return $this
|
373 |
| - * |
374 |
| - * @throws InvalidArgumentException If $callback is not callable or if $setterCallback is set and not callable |
375 | 375 | */
|
376 |
| - public function registerAttributeCallback($name, $callback, $setterCallback = null) |
| 376 | + public function registerAttributeCallback(string $name, ?callable $callback, ?callable $setterCallback = null): self |
377 | 377 | {
|
378 | 378 | if ($callback !== null) {
|
379 |
| - if (! is_callable($callback)) { |
380 |
| - throw new InvalidArgumentException(__METHOD__ . ' expects a callable callback'); |
381 |
| - } |
382 |
| - |
383 | 379 | $this->callbacks[$name] = $callback;
|
384 | 380 | }
|
385 | 381 |
|
386 | 382 | if ($setterCallback !== null) {
|
387 |
| - if (! is_callable($setterCallback)) { |
388 |
| - throw new InvalidArgumentException(__METHOD__ . ' expects a callable setterCallback'); |
389 |
| - } |
390 |
| - |
391 | 383 | $this->setterCallbacks[$name] = $setterCallback;
|
392 | 384 | }
|
393 | 385 |
|
@@ -518,4 +510,58 @@ public function getIterator(): Traversable
|
518 | 510 | {
|
519 | 511 | return new ArrayIterator($this->attributes);
|
520 | 512 | }
|
| 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 | + } |
521 | 567 | }
|
0 commit comments