devstackphp/di

View on GitHub
src/Definition/Source/PhpDocReader.php

Summary

Maintainability
C
1 day
Test Coverage
<?php
/**
 * This file is part of the Stack package.
 *
 * (c) Andrzej Kostrzewa <andkos11@gmail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Stack\DI\Definition\Source;

use Stack\DI\Definition\ObjectDefinition;
use Stack\DI\Exception\AnnotationException;

/**
 * PhpDoc reader.
 *
 * @author Andrzej Kostrzewa <andkos11@gmail.com>
 */
class PhpDocReader
{
    /**
     * @var array
     */
    protected static $ignoredTypes = [
        'bool',
        'boolean',
        'string',
        'int',
        'integer',
        'float',
        'double',
        'array',
        'object',
        'callable',
        'resource',
        '',
    ];

    /**
     * @var string
     */
    private $namespace;

    /**
     * Parse the docblock of the property to get the parameters of the param annotation.
     *
     * @param \ReflectionMethod $method
     *
     * @return array|null
     */
    public function getMethodParameters(\ReflectionMethod $method)
    {
        $methodComment = $method->getDocComment();
        if (!preg_match_all('/@param\s+([^\s\*\/]+)/', $methodComment, $matches)) {
            return;
        }

        $classNames = end($matches);

        if (!is_array($classNames)) {
            return;
        }

        $parameters = [];
        foreach ($classNames as $type) {
            $values = explode('(', $type);
            if (in_array($values[0], static::$ignoredTypes)) {
                $value = end($values);
                $value = trim($value, ') ');
                $type  = static::parseValue($value);
            }
            $parameters[] = $type;
        }

        return $parameters;
    }

    /**
     * Parse the docblock of the property to get the class of the var annotation.
     *
     * @param \ReflectionProperty $property
     *
     * @throws AnnotationException Non exists class.
     *
     * @return null|ObjectDefinition
     */
    public function getPropertyClass(\ReflectionProperty $property)
    {
        $propertyComment = $property->getDocComment();
        if (!preg_match('/@var\s+([^\s\(\*\/]+)/', $propertyComment, $matches)) {
            return;
        }

        $className = end($matches);

        if (!is_string($className) || in_array($className, static::$ignoredTypes)) {
            return;
        }

        $classWithNamespace = $className;
        if ($this->namespaceExists($classWithNamespace) === false) {
            $classWithNamespace = $this->namespace.'\\'.$className;
        }

        if (!$this->classExists($classWithNamespace)) {
            $declaringClass = $property->getDeclaringClass();
            throw new AnnotationException(sprintf(
                'The @var annotation on %s::%s contains a non existent class "%s"',
                $declaringClass->name,
                $property->getName(),
                $className
            ));
        }

        $createNewObject = function ($propertyComment, $className, $classWithNamespace) {
            $classParameters = $this->propertyClassParameters($propertyComment, $className);
            if (is_array($classParameters)) {
                $values = [];
                foreach ($classParameters as $value) {
                    $values[] = static::parseValue($value);
                }

                $object = new ObjectDefinition($classWithNamespace, $className);
                $object->setConstructorInjection($values);

                return $object;
            }

            return new $classWithNamespace();
        };

        return $createNewObject($propertyComment, $className, $classWithNamespace);
    }

    /**
     * @param string|false $className
     *
     * @return bool
     */
    private function classExists($className)
    {
        return class_exists($className) || interface_exists($className);
    }

    /**
     * @param string|false $className
     *
     * @return int
     */
    private function namespaceExists($className)
    {
        return strpos($className, $this->namespace);
    }

    /**
     * Parse value by type and return.
     *
     * @param $value
     *
     * @return string
     */
    protected static function parseValue($value)
    {
        $value = trim($value, ', ');

        $isNumberOrBool = function (&$value) {
            if (is_numeric($value)) {
                $value = (float) $value;

                if ((float) $value == (int) $value) {
                    $value = (int) $value;
                }

                return true;
            }

            $isBool = function (&$value) {
                if (strtolower($value) == 'true') {
                    $value = true;

                    return true;
                }

                if (strtolower($value) == 'false') {
                    $value = false;

                    return true;
                }

                return false;
            };

            return $isBool($value);
        };

        $isArrayOrOther = function (&$value) {
            if (substr($value, 0, 1) === '[' && substr($value, -1) === ']') {
                $valuesArray = explode(',', substr($value, 1, -1));
                $value       = [];
                foreach ($valuesArray as $val) {
                    $value[] = static::parseValue($val);
                }

                return true;
            }

            if (substr($value, 0, 1) == '"' && substr($value, -1) == '"' ||
                substr($value, 0, 1) == '\'' && substr($value, -1) == '\'') {
                $value = substr($value, 1, -1);
                $value = static::parseValue($value);

                return true;
            }

            return false;
        };

        if ($isArrayOrOther($value)) {
            return $value;
        }

        if ($isNumberOrBool($value)) {
            return $value;
        }

        return $value;
    }

    /**
     * Get property class parameters.
     *
     * @param string       $property
     * @param string|false $className
     *
     * @return string[]|false|null
     */
    private function propertyClassParameters($property, $className)
    {
        $classNamePosition = strpos($property, $className);
        if ($classNamePosition !== false) {
            $classNameLength = mb_strlen($className);
            $property        = ltrim(substr($property, $classNamePosition + $classNameLength));
            if (preg_match_all('/([\w,$\[\]\'\"]+)/', $property, $matches)) {
                return end($matches);
            }
        }
    }

    /**
     * Set default namespace.
     *
     * @param $namespace
     */
    public function setNamespace($namespace)
    {
        $this->namespace = $namespace;
    }
}