src/Compiler.php

Summary

Maintainability
A
3 hrs
Test Coverage
<?php

declare(strict_types=1);

namespace Cekta\DI;

use InvalidArgumentException;
use UnexpectedValueException;

class Compiler
{
    /**
     * @var array<string, mixed>
     */
    private array $params;
    /**
     * @var array<string, string>
     */
    private array $alias;
    /**
     * @var array<string, callable>
     */
    private array $definitions;
    private string $fqcn;
    /**
     * @var array<string>
     */
    private array $shared = [];
    /**
     * @var array<string, array<array{'name': string, 'variadic': bool, parameter: string}>>
     */
    private array $dependenciesMap = [];
    private Reflection $reflection;
    private array $stack = [];

    /**
     * @param array<string, mixed> $params
     * @param array<string, string> $alias
     * @param array<string, callable> $definitions
     * @param string $fqcn
     */
    public function __construct(array $params, array $alias, array $definitions, string $fqcn)
    {
        $this->params = $params;
        $this->alias = $alias;
        $this->definitions = $definitions;
        $this->fqcn = $fqcn;
    }

    /**
     * @param array<string> $containers
     * @param bool $definition_overridable
     * @param bool $alias_overridable
     * @param bool $reflection_enabled
     * @return string|false
     */
    public function __invoke(
        array $containers,
        bool $definition_overridable,
        bool $alias_overridable,
        bool $reflection_enabled
    ): string|false {
        $this->reflection = new Reflection();
        $namespace = $this->getNamespace();
        $class = $this->getClass();
        $alias = $this->alias;
        $definitions = $this->definitions;
        $containers = $this->generateContainers($containers);
        ob_start();
        include __DIR__ . '/../template/container.compiler.php';
        return ob_get_clean();
    }

    /**
     * @return string
     */
    private function getNamespace(): string
    {
        $position = strrpos($this->fqcn, '\\');
        if ($position === false) {
            throw new InvalidArgumentException("Invalid fqcn: `$this->fqcn` must contain \\");
        }
        return substr($this->fqcn, 0, $position);
    }

    /**
     * @return string
     */
    private function getClass(): string
    {
        return substr($this->fqcn, strrpos($this->fqcn, '\\') + 1);
    }

    /**
     * @param string[] $targets
     * @return array<string, string>
     */
    private function generateContainers(array $targets): array
    {
        $this->generateMap($targets);
        $containers = [];
        foreach (array_merge($targets, $this->shared, $this->alias) as $target) {
            $containers[$target] = $this->buildContainer($target);
        }
        return $containers;
    }

    /**
     * @param array<string> $containers
     */
    private function generateMap(array $containers): void
    {
        foreach ($containers as $container) {
            if (array_key_exists($container, $this->alias)) {
                $container = $this->alias[$container];
            }
            if (
                array_key_exists($container, $this->params)
                || array_key_exists($container, $this->definitions)
                || array_key_exists($container, $this->shared)
            ) {
                continue;
            }
            if (array_key_exists($container, $this->dependenciesMap)) {
                $this->shared[] = $container;
                continue;
            }
            if ($this->reflection->isInstantiable($container)) {
                $this->dependenciesMap[$container] = $this->reflection->getDependencies($container);
                /** @var array<string> $new_containers */
                $new_containers = [];
                foreach ($this->dependenciesMap[$container] as $dependency) {
                    $new_containers[] = array_key_exists($dependency['parameter'], $this->alias) ?
                        $dependency['parameter'] : $dependency['name'];
                }
                $this->generateMap($new_containers);
                continue;
            }
            throw new UnexpectedValueException("`$container` is cant be resolved");
        }
    }

    private function buildContainer(string $target): string
    {
        if (
            (in_array($target, $this->shared) && count($this->stack) !== 0)
            || array_key_exists($target, $this->alias)
            || array_key_exists($target, $this->definitions)
            || array_key_exists($target, $this->params)
        ) {
            return "\$this->get('$target')";
        }
        $this->stack[] = $target;
        $container = "new \\$target(";
        if (array_key_exists($target, $this->dependenciesMap)) {
            foreach ($this->dependenciesMap[$target] as $dependency) {
                $name = array_key_exists(
                    $dependency['parameter'],
                    $this->alias
                ) ? $dependency['parameter'] : $dependency['name'];
                $variadic = $dependency['variadic'] === true ? '...' : '';
                $container .= "$variadic{$this->buildContainer($name)}, ";
            }
        }
        $container .= ')';
        array_pop($this->stack);
        return $container;
    }
}