AutoMapper.php 5.73 KB
<?php

namespace FootyRoom\Support;

use ArrayObject;
use ReflectionClass;
use ReflectionObject;
use ReflectionProperty;
use MongoDB\Model\BSONArray;

class AutoMapper
{
    /**
     * Maps properties from given object to a new instance of specified type.
     *
     * Only public properties of source will be mapped to target. Target
     * properties can still be private. If target contains properties with
     * interal PHP classes then those will not be mapped. Primitive types are
     * mapped without casting.
     *
     * @param mixed $source
     * @param string $name Class name
     * @param array $mappings Mappings in ['to' => 'from'] configuration.
     * @param bool $merge If set to true, AutoMapper will use user defined
     *                    mappings as an addition to automatic same name
     *                    mappings. If set to false then only user defined
     *                    mappings will be performed.
     *
     * @return mixed
     */
    public static function map($source, $name, $mappings = [], $merge = true)
    {
        $targetReflection = new ReflectionClass($name);

        // We don't map PHP internal classes.
        if ($targetReflection->isInternal()) {
            return;
        }

        $target = $targetReflection->newInstanceWithoutConstructor();

        if (empty($mappings) || $merge) {
            $properties = $targetReflection->getProperties();

            foreach ($properties as $prop) {
                if (!isset($mappings[$prop->getName()])) {
                    $mappings[$prop->getName()] = $prop->getName();
                }
            }
        }

        foreach ($mappings as $to => $from) {
            self::mapProperty($source, $target, $targetReflection, $from, $to);
        }

        return $target;
    }

    /**
     * Sets a non-public property to a specified value.
     *
     * @param mixed $target
     * @param string $key
     * @param mixed $value
     */
    public static function setValue($target, $key, $value)
    {
        $targetReflection = new ReflectionObject($target);

        $targetProperty = $targetReflection->getProperty($key);
        $targetProperty->setAccessible(true);
        $targetProperty->setValue($target, $value);
    }

    /**
     * Maps single property from source to target.
     *
     * @param object $source
     * @param mixed $target
     * @param \ReflectionClass $targetReflection
     * @param string $from Property name on source
     * @param string $to Property name on target
     */
    protected static function mapProperty($source, $target, $targetReflection, $from, $to)
    {
        if ($source === null || !property_exists($source, $from)) {
            return;
        }

        $targetProperty = $targetReflection->getProperty($to);
        $targetProperty->setAccessible(true);

        $sourceProperty = $source->{$from};

        $sourceClass = null;
        $targetClass = null;
        $targetArrayClass = null;
        $isPrimitiveType = true;

        // We will only parse type of target if source is an object or array
        // because if it's not then we wont cast premitive type into an object
        // anyway.
        if (is_object($sourceProperty) && !($sourceProperty instanceof BSONArray)) {
            $targetClass = self::parseType($targetProperty);

            if ($targetClass) {
                $isPrimitiveType = self::isPrimitiveType($targetClass);
                $sourceClass = get_class($sourceProperty);
            }
        } elseif (is_array($sourceProperty) || $sourceProperty instanceof ArrayObject) {
            $targetArrayClass = self::parseArrayType($targetProperty);
            $isPrimitiveType = self::isPrimitiveType($targetArrayClass);
        }

        // If a target is an object.
        if ($targetClass && $targetClass !== $sourceClass && !$isPrimitiveType) {
            $targetProperty->setValue(
                $target,
                self::map($sourceProperty, $targetClass)
            );
        }
        // If target is an array of objects.
        elseif ($targetArrayClass && !$isPrimitiveType) {
            $items = [];

            foreach ($sourceProperty as $item) {
                $items[] = self::map($item, $targetArrayClass);
            }

            $targetProperty->setValue($target, $items);
        } else {
            $targetProperty->setValue($target, $sourceProperty);
        }
    }

    /**
     * Returns type of property.
     *
     * @param \ReflectionProperty $property
     *
     * @return string|null
     */
    protected static function parseType(ReflectionProperty $property)
    {
        $result = preg_match('/@var\\s*([^\\s\\|]*)/i', $property->getDocComment(), $matches);

        return isset($matches[1]) ? trim($matches[1], '\\') : null;
    }

    /**
     * Returns type of typed array property.
     *
     * @param \ReflectionProperty $property
     *
     * @return string|null
     */
    protected static function parseArrayType(ReflectionProperty $property)
    {
        $result = preg_match('/@var\\s*([^\\s\\[]*)\\[\\]/i', $property->getDocComment(), $matches);

        return isset($matches[1]) ? trim($matches[1], '\\') : null;
    }

    /**
     * Determines if type is one of PHP's primitive types.
     *
     * @param string $type
     *
     * @return bool
     */
    protected static function isPrimitiveType($type)
    {
        switch (strtolower($type)) {
            case 'int':
            case 'integer':
            case 'bool':
            case 'boolean':
            case 'float':
            case 'double':
            case 'real':
            case 'string':
            case 'array':
            case 'object':
            case 'callable':
            case 'mixed':
                return true;
            default:
                return false;
        }
    }
}