classes/Resource/ResourceManager.php

Summary

Maintainability
A
1 hr
Test Coverage
<?php
/**
 * Fixin Framework
 *
 * Copyright (c) Attila Jenei
 *
 * http://www.fixinphp.com
 */

namespace Fixin\Resource;

use Fixin\Resource\AbstractFactory\AbstractFactory;
use Fixin\Support\Ground;

class ResourceManager implements ResourceManagerInterface
{
    protected const
        CLASS_KEY = 'class',
        CLASS_NOT_FOUND_EXCEPTION = "Class not found for '%s'",
        INJECT_KEYS = [
            self::DEFINITIONS,
            self::RESOURCES,
        ],
        INVALID_ABSTRACT_FACTORY_DEFINITION_EXCEPTION = "Invalid abstract factory definition '%s'",
        OPTIONS_KEY = 'options',
        PROTOTYPE_AS_RESOURCE_EXCEPTION = "Can't use prototype as resource '%s'",
        PROTOTYPE_NOT_FOUND_EXCEPTION = "Prototype not found by name '%s'",
        RESOURCE_AS_PROTOTYPE_EXCEPTION = "Can't use resource as prototype '%s'",
        RESOURCE_NOT_FOUND_EXCEPTION = "Resource not found by name '%s'",
        UNEXPECTED_RESOURCE_EXCEPTION = "Unexpected resource for name '%s' (%s), '%s' expected";

    public const
        ABSTRACT_FACTORIES = 'abstractFactories',
        DEFINITIONS = 'definitions',
        RESOURCES = 'resources';

    /**
     * @var AbstractFactory
     */
    protected $abstractFactoryChain;

    /**
     * @var string[][]
     */
    protected $abstractFactoryDefinitions = [];

    /**
     * @var array
     */
    protected $abstractFactoryMappings = [];

    /**
     * @var array
     */
    protected $definitions;

    /**
     * @var bool[]
     */
    protected $hasTests = [];

    /**
     * @var array
     */
    protected $resources;

    public function __construct(array $options)
    {
        // Abstract factories
        if (isset($options[static::ABSTRACT_FACTORIES])) {
            $this->abstractFactoryDefinitions = $options[static::ABSTRACT_FACTORIES];
        }

        // Inject options
        foreach (static::INJECT_KEYS as $key) {
            $this->$key = $options[$key] ?? [];
        }
    }

    public function __toString(): string
    {
        $resources = [];

        foreach ($this->definitions as $key => $definition) {
            $resources[$key] = str_pad($key, 50) . ' defined';
        }

        foreach ($this->resources as $key => $resource) {
            $resources[$key] = str_pad($key, 50) . ' {' . get_class($resource) . '} ' . ($resource instanceof ResourceInterface ? 'resource' : 'prototype');
        }

        ksort($resources);

        return Ground::toDebugBlock(get_class($this) . ' {' . PHP_EOL . PHP_EOL . '    ' . implode(',' . PHP_EOL . '    ', $resources) . PHP_EOL . '}');
    }

    protected function canProduceByAbstractFactories(string $key): bool
    {
        return ($this->abstractFactoryChain ?? $this->prepareAbstractFactoryChain())->canChainProduce($key);
    }

    /**
     * @throws Exception\ResourceNotFoundException
     */
    public function clone(string $name, string $expectedClass, array $options = [])
    {
        $resource = $this->prepareResource($name, $expectedClass);

        if ($resource instanceof PrototypeInterface) {
            return $resource->withOptions($options);
        }

        if (!$resource instanceof ResourceInterface) {
            return clone $resource;
        }

        throw new Exception\ResourceNotFoundException(sprintf(static::RESOURCE_AS_PROTOTYPE_EXCEPTION, $name));
    }

    /**
     * @throws Exception\ResourceNotFoundException
     */
    public function get(string $name, string $expectedClass)
    {
        $resource = $this->prepareResource($name, $expectedClass);

        if ($resource instanceof ResourceInterface) {
            return $resource;
        }

        throw new Exception\ResourceNotFoundException(sprintf(static::PROTOTYPE_AS_RESOURCE_EXCEPTION, $name));
    }

    public function has(string $name): bool
    {
        return $this->hasTests[$name] ?? $this->hasTests[$name] = isset($this->resources[$name]) || isset($this->definitions[$name]) || $this->canProduceByAbstractFactories($name);
    }

    protected function prepareAbstractFactoryChain(): AbstractFactory
    {
        $definitions = $this->abstractFactoryDefinitions;
        $this->abstractFactoryDefinitions = [];

        foreach (array_reverse($definitions) as $key => $definition) {
            $class = $definition[static::CLASS_KEY] ?? $definition;

            $abstractFactory = new $class($this, [AbstractFactory::NEXT => $this->abstractFactoryChain] + ($definition[static::OPTIONS_KEY] ?? []), $key);

            if (!$abstractFactory instanceof AbstractFactory) {
                throw new Exception\InvalidArgumentException(sprintf(static::INVALID_ABSTRACT_FACTORY_DEFINITION_EXCEPTION, $key));
            }

            $this->abstractFactoryChain = $abstractFactory;
        }

        return $this->abstractFactoryChain;
    }

    protected function prepareResource(string $name, string $expectedClass)
    {
        $resource = $this->resources[$name] ?? $this->resources[$name] = $this->produceResource($name, [], $name);

        if ($resource instanceof $expectedClass) {
            return $resource;
        }

        throw new Exception\UnexpectedResourceException(sprintf(static::UNEXPECTED_RESOURCE_EXCEPTION, $name, $resource ? get_class($resource) : 'null', $expectedClass));
    }

    protected function produceResource(string $key, array $options, string $name)
    {
        if (isset($this->definitions[$key])) {
            $definition = $this->definitions[$key];

            if (is_string($definition)) {
                return $this->produceResource($definition, $options, $name);
            }
            elseif (is_array($definition)) {
                if (isset($definition[static::OPTIONS_KEY])) {
                    $options += $definition[static::OPTIONS_KEY];
                }

                if (isset($definition[static::CLASS_KEY])) {
                    return $this->produceResource($definition[static::CLASS_KEY], $options, $name);
                }
            }
            elseif ($definition instanceof \Closure) {
                return $definition($this, $options, $key);
            }
        }

        $instance = ($this->abstractFactoryChain ?? $this->prepareAbstractFactoryChain())->chainProduce($key, $options, $name);

        if ($instance === null) {
            throw new Exception\ClassNotFoundException(sprintf(static::CLASS_NOT_FOUND_EXCEPTION, $name));
        }

        return $instance instanceof FactoryInterface || $instance instanceof \Closure ? $instance($this, $options, $name) : $instance;
    }
}