File "CollectionAnnotationReader.php"

Full Path: /var/www/html/back/vendor/spatie/laravel-data/src/Support/Annotations/CollectionAnnotationReader.php
File size: 4.74 KB
MIME-type: text/x-php
Charset: utf-8

<?php

namespace Spatie\LaravelData\Support\Annotations;

use Iterator;
use IteratorAggregate;
use phpDocumentor\Reflection\DocBlockFactory;
use phpDocumentor\Reflection\TypeResolver;
use phpDocumentor\Reflection\Types\Context;
use ReflectionClass;
use Spatie\LaravelData\Contracts\BaseData;
use Spatie\LaravelData\Resolvers\ContextResolver;

class CollectionAnnotationReader
{
    /** @var array<class-string, CollectionAnnotation|null> */
    protected static array $cache = [];

    protected Context $context;

    public function __construct(
        protected readonly ContextResolver $contextResolver,
        protected readonly TypeResolver $typeResolver,
    ) {
    }

    /**
     * @param class-string $className
     */
    public function getForClass(string $className): ?CollectionAnnotation
    {
        if (array_key_exists($className, self::$cache)) {
            return self::$cache[$className];
        }

        $class = new ReflectionClass($className);

        if (empty($class->getDocComment())) {
            return self::$cache[$className] = null;
        }

        if (! $this->isIterable($class)) {
            return self::$cache[$className] = null;
        }

        $type = $this->getCollectionReturnType($class);

        if ($type === null || $type['valueType'] === null) {
            return self::$cache[$className] = null;
        }

        return self::$cache[$className] = new CollectionAnnotation(
            type: $type['valueType'],
            isData: class_exists($type['valueType']) && in_array(BaseData::class, class_implements($type['valueType'])),
            keyType: $type['keyType'] ?? 'array-key',
        );
    }

    public static function clearCache(): void
    {
        self::$cache = [];
    }

    protected function isIterable(ReflectionClass $class): bool
    {
        $collectionInterfaces = [
            Iterator::class,
            IteratorAggregate::class,
        ];

        foreach ($collectionInterfaces as $interface) {
            if ($class->implementsInterface($interface)) {
                return true;
            }
        }

        return false;
    }

    /**
     * @return array{keyType: string|null, valueType: string|null}|null
     */
    protected function getCollectionReturnType(ReflectionClass $class): ?array
    {
        $docBlockFactory = DocBlockFactory::createInstance();
        $this->context = $this->contextResolver->execute($class);
        $docComment = $class->getDocComment();

        if ($docComment === false) {
            return null;
        }

        $docBlock = $docBlockFactory->create($docComment, $this->context);

        $templateTypes = [];

        foreach ($docBlock->getTags() as $tag) {
            $tagName = $tag->getName();
            $description = (string) $tag;

            if ($tagName === 'template') {
                $this->processTemplateTag($description, $templateTypes);

                continue;
            }

            if ($tagName === 'extends') {
                return $this->processExtendsTag($description, $templateTypes);
            }
        }

        return null;
    }

    private function processTemplateTag(string $description, array &$templateTypes): void
    {
        // The pattern matches strings like "T of SomeType", capturing "T" as the template name
        // and "SomeType" as the type associated with the template.
        if (preg_match('/^(\w+)\s+of\s+([^\s]+)/', $description, $matches)) {
            $templateTypes[$matches[1]] = $this->resolve($matches[2]);
        }
    }

    private function processExtendsTag(string $description, array $templateTypes): ?array
    {
        // The pattern matches strings like "<KeyType, ValueType>" or "<ValueType>",
        // capturing "KeyType" and "ValueType" or just "ValueType" if no key type is specified.
        if (preg_match('/<\s*([^,\s]+)?\s*(?:,\s*([^>\s]+))?\s*>/', $description, $matches)) {
            $keyType = null;
            $valueType = null;

            if (count($matches) === 3) {
                $keyType = $templateTypes[class_basename($matches[1])] ?? $this->resolve($matches[1]);
                $valueType = $templateTypes[class_basename($matches[2])] ?? $this->resolve($matches[2]);
            } else {
                $valueType = $templateTypes[class_basename($matches[1])] ?? $this->resolve($matches[1]);
            }

            $keyType = $keyType ? explode('|', $keyType)[0] : null;
            $valueType = explode('|', $valueType)[0];

            return [
                'keyType' => $keyType,
                'valueType' => $valueType,
            ];
        }

        return null;
    }

    protected function resolve(string $type): ?string
    {
        $type = (string) $this->typeResolver->resolve($type, $this->context);

        return $type ? ltrim($type, '\\') : null;
    }
}