Stratadox/Hydrator

View on GitHub
src/ScopedHydrator.php

Summary

Maintainability
A
0 mins
Test Coverage
<?php declare(strict_types=1);

namespace Stratadox\Hydrator;

use InvalidArgumentException;
use ReflectionClass;
use ReflectionException;
use ReflectionObject;
use function sprintf;
use function strlen;
use function strpos;
use function substr;
use Throwable;

/**
 * Hydrator properties in a specific scope by deconstructing the input.
 *
 * Useful in the specific case where a child class has a private property, while
 * its parent(s) also have a private property by that very same name.
 * In those edge cases, the reflective hydrator cannot correctly determine the
 * property scope, nor would client code be able to pass both properties in the
 * same map.
 * This hydrator avoids that problem by requiring an explicit scope in the form
 * of one or more subsequent prefixes, for example:
 * `{"property": "foo", "parent.property": "bar"}`
 *
 * @package Stratadox\Hydrate
 * @author  Stratadox
 */
final class ScopedHydrator implements Hydrator
{
    /** @var string */
    private $prefix;
    /** @var int */
    private $prefixLength;

    private function __construct(string $prefix)
    {
        $this->prefix = $prefix;
        $this->prefixLength = strlen($prefix);
        if (!$this->prefixLength) {
            throw new InvalidArgumentException(
                'The prefix for the scoped hydrator cannot be empty.'
            );
        }
    }

    /**
     * Produce a scoped hydrator.
     *
     * @return Hydrator A hydrator that uses prefixes to determine the scopes.
     */
    public static function default(): Hydrator
    {
        return new self('parent.');
    }

    /**
     * Produce a scoped hydrator with a custom prefix.
     *
     * @param string $prefix  The prefix to determine the parental scope.
     * @return ScopedHydrator A hydrator that uses the custom prefix.
     */
    public static function prefixedWith(string $prefix): self
    {
        return new self($prefix);
    }

    /** @inheritdoc */
    public function writeTo(object $target, array $data): void
    {
        $object = new ReflectionObject($target);
        foreach ($data as $name => $value) {
            try {
                $this->write($object, $target, $name, $value);
            } catch (Throwable $exception) {
                throw HydrationFailed::encountered($exception, $target);
            }
        }
    }

    /**
     * @param mixed $value
     * @var ReflectionClass|bool $class
     * @throws ReflectionException
     */
    private function write(
        ReflectionClass $class,
        object $target,
        string $propertyName,
        $value
    ): void {
        $name = $propertyName;
        while (strpos($name, $this->prefix) === 0) {
            $name = substr($name, $this->prefixLength);
            $class = $class->getParentClass();
            if (!$class) {
                throw new InvalidArgumentException(sprintf(
                    'It has no %s.',
                    $propertyName
                ));
            }
        }
        $property = $class->getProperty($name);
        $property->setAccessible(true);
        $property->setValue($target, $value);
    }
}