norse-blue/php-extensible-objects

View on GitHub
src/Extension.php

Summary

Maintainability
A
0 mins
Test Coverage
A
91%
<?php

declare(strict_types=1);

namespace NorseBlue\ExtensibleObjects;

use Closure;
use JetBrains\PhpStorm\ArrayShape;
use NorseBlue\ExtensibleObjects\Exceptions\MethodNotBindableException;
use NorseBlue\HandyProperties\Traits\HasPropertyAccessors;
use stdClass;

/**
 * @property-read bool $is_guarded
 * @property-read bool $is_static
 * @property-read callable $method
 */
final class Extension
{
    use HasPropertyAccessors;

    protected bool $guarded;

    /** @var callable */
    protected $method;

    protected bool $static;

    /**
     * Create a new instance.
     */
    public function __construct(callable $method, bool $guarded)
    {
        $this->method = $method;
        $this->guarded = $guarded;

        $closure = $method instanceof Closure ? $method : Closure::fromCallable($method());
        $this->static = $this->isClosureStatic($closure);
    }

    /**
     * Execute the extension method.
     *
     * @param string $scope The new scope of the extension method.
     * @param array<mixed> $parameters The extension method parameters to use.
     * @param object|null $caller The caller object.
     */
    public function execute(string $scope, array $parameters, ?object $caller = null): mixed
    {
        $method = $this->method;

        if ($this->is_static) {
            $caller = null;
        }

        $closure = Closure::fromCallable($method())
            ->bindTo($caller, $scope);

        return $closure(...$parameters);
    }

    /**
     * Get the extension as an array.
     *
     * @return array<string, mixed>
     */
    #[ArrayShape(['method' => 'callable', 'static' => 'bool', 'guarded' => 'bool'])]
    public function toArray(): array
    {
        return [
            'method' => $this->method,
            'static' => $this->static,
            'guarded' => $this->guarded,
        ];
    }

    protected function accessorIsGuarded(): bool
    {
        return $this->guarded;
    }

    protected function accessorIsStatic(): bool
    {
        return $this->static;
    }

    protected function accessorMethod(): callable
    {
        return $this->method;
    }

    /**
     * Determine if the closure is static.
     */
    protected function isClosureStatic(Closure $closure): bool
    {
        set_error_handler(
            static function (int $errno, string $errstr, string $errfile, int $errline): bool {
                if ($errstr === 'Cannot bind an instance to a static closure') {
                    throw new MethodNotBindableException($errstr, $errno, $errfile, $errline);
                }

                return true;
            }
        );

        try {
            $closure->bindTo(new stdClass());
        } catch (MethodNotBindableException) {
            return true;
        } finally {
            restore_error_handler();
        }

        return false;
    }
}