Newer
Older
TillQliro / Model / ContainerMapper.php
@Jonas Jonsson Jonas Jonsson on 2 Apr 2024 7 KB Initial
<?php
/**
 * Copyright © Qliro AB. All rights reserved.
 * See LICENSE.txt for license details.
 */

// @codingStandardsIgnoreFile
// phpcs:ignoreFile

namespace Qliro\QliroOne\Model;

use Magento\Framework\ObjectManagerInterface;
use Qliro\QliroOne\Api\Data\ContainerInterface;

/**
 * Container mapper class
 */
class ContainerMapper
{
    /**
     * @var array
     */
    private $setterTypeCache = [];

    /**
     * @var array
     */
    private $getterCache = [];

    /**
     * @var array
     */
    private $setterCache = [];

    /**
     * @var \Magento\Framework\ObjectManagerInterface
     */
    private $objectManager;

    /**
     * @var \Qliro\QliroOne\Model\Logger\Manager
     */
    private $logManager;

    /**
     * Inject dependencies
     *
     * @param \Magento\Framework\ObjectManagerInterface $objectManager
     * @param \Qliro\QliroOne\Model\Logger\Manager $logManager
     */
    public function __construct(
        ObjectManagerInterface $objectManager,
        Logger\Manager $logManager
    ) {
        $this->objectManager = $objectManager;
        $this->logManager = $logManager;
    }

    /**
     * Recursively convert an instance of container into associative array ready for JSON payload
     *
     * @param \Qliro\QliroOne\Api\Data\ContainerInterface $container
     * @param array $mandatoryFields
     * @return array
     */
    public function toArray(ContainerInterface $container, $mandatoryFields = [])
    {
        $result = [];

        foreach ($this->getGetterKeys($container) as $key) {
            $getterName = 'get' . $key;
            $value = $container->$getterName();

            if (in_array($key, $mandatoryFields, true) || !is_null($value)) {
                if ($this->checkIfArrayWithNumericKeys($value)) {
                    $value = $this->iterateArray($value);
                } elseif ($value instanceof ContainerInterface) {
                    $value = $this->toArray($value);
                }

                $result[$key] = $value;
            }
        }

        return $result;
    }

    /**
     * @param array $data
     * @param \Qliro\QliroOne\Api\Data\ContainerInterface|string $container
     * @return \Qliro\QliroOne\Api\Data\ContainerInterface
     */
    public function fromArray($data, $container)
    {
        if (is_string($container)) {
            $container = $this->objectManager->create($container);
        }

        $className = get_class($container);
        $keyHash = array_flip($this->getSetterKeys($container));

        if (is_null($data)) {
            $this->logManager->debug(
                'Unexpected null',
                [
                    'extra' => sprintf('%s; %s', get_class($container), $this->logManager->getStack())
                ]
            );
        }
        foreach ($data as $key => $value) {
            $key = ucfirst($key);
            $setterName = 'set' . $key;

            if (isset($keyHash[$key])) {
                $setterType = $this->setterTypeCache[$className][$key] ?? null;

                if ($this->checkIfArrayWithNumericKeys($value)) {
                    $value = $this->iterateArray($value, $setterType);
                } elseif ($setterType) {
                    // If value is an associative array, wrap it into container
                    $subClassName = rtrim($setterType, '[]');
                    $subContainer = $this->fromArray($value, $subClassName);
                    $value = $subContainer;
                }

                $container->$setterName($value);
            }
        }

        return $container;
    }

    /**
     * Fetch a cached list of data fields in container that have getters
     *
     * @param \Qliro\QliroOne\Api\Data\ContainerInterface $container
     * @return array
     */
    private function getGetterKeys(ContainerInterface $container)
    {
        $className = get_class($container);

        if (!isset($this->getterCache[$className])) {
            $this->collectAccessors($container);
        }

        return $this->getterCache[$className];
    }

    /**
     * Fetch a cached list of data fields in container that have setters
     *
     * @param \Qliro\QliroOne\Api\Data\ContainerInterface $container
     * @return array
     */
    private function getSetterKeys(ContainerInterface $container)
    {
        $className = get_class($container);

        if (!isset($this->setterCache[$className])) {
            $this->collectAccessors($container);
        }

        return $this->setterCache[$className];
    }

    /**
     * @param \Qliro\QliroOne\Api\Data\ContainerInterface $container
     */
    private function collectAccessors(ContainerInterface $container)
    {
        $className = get_class($container);
        $collectedGetters = [];
        $collectedSetters = [];

        foreach (get_class_methods($container) as $classMethod) {
            if (preg_match('/^get([A-Z].*)$/', $classMethod, $matches)) {
                $collectedGetters[] = $matches[1];
            } elseif (preg_match('/^set([A-Z].*)$/', $classMethod, $matches)) {
                $key = $matches[1];
                $collectedSetters[] = $key;
                $setterName = 'set' . $key;

                try {
                    $method = new \ReflectionMethod($container, $setterName);
                    $params = $method->getParameters();
                    $setterClass = $params[0]->getClass();
                    $setterType = $setterClass ? $setterClass->getName() : null;

                    if (!$setterType) {
                        $doc = $method->getDocComment();

                        if (preg_match('/@param\s+([^\s]+)\s+\$' . $params[0]->getName() . '/', $doc, $matches)) {
                            $setterType = $matches[1];

                            if (strpos($setterType, '\\') === false) {
                                $class = new \ReflectionClass($container);
                                $namespace = $class->getNamespaceName();
                                $setterType = ltrim(implode('\\', [trim($namespace, '\\'), $setterType]), '\\');

                                if (!class_exists(rtrim($setterType, '[]'))) {
                                    $setterType = null;
                                }
                            }
                        }
                    }
                } catch (\ReflectionException $e) {
                    $setterType = null;
                }

                $this->setterTypeCache[$className][$key] = $setterType;
            }
        }

        $this->getterCache[$className] = $collectedGetters;
        $this->setterCache[$className] = $collectedSetters;
    }

    /**
     * Iterate an array and convert its elements
     *
     * @param array $data
     * @param bool|string $setterType
     * @return array
     */
    private function iterateArray($data, $setterType = false)
    {
        foreach ($data as $key => $value) {
            if ($this->checkIfArrayWithNumericKeys($value)) {
                // If the value a numeric key array, iterate again each element of it
                $value = $this->iterateArray($value, $setterType);
            } elseif ($setterType === false && ($value instanceof ContainerInterface)) {
                // If value is a container, convert it to array
                $value = $this->toArray($value);
            } elseif (is_array($value)) {
                // If value is an associative array, wrap it into container
                $className = rtrim($setterType, '[]');
                $container = $this->fromArray($value, $className);
                $value = $container;
            }

            $data[$key] = $value;
        }

        return $data;
    }

    /**
     * Check if the argument is an array with numeric keys starting with 0
     *
     * @param array $item
     * @return bool
     */
    private function checkIfArrayWithNumericKeys($item)
    {
        return is_array($item) && isset($item[0]);
    }
}