wol-soft/php-json-schema-model-generator

View on GitHub
src/Model/Property/Property.php

Summary

Maintainability
A
3 hrs
Test Coverage
A
100%
<?php

declare(strict_types = 1);

namespace PHPModelGenerator\Model\Property;

use PHPModelGenerator\Exception\SchemaException;
use PHPModelGenerator\Model\Schema;
use PHPModelGenerator\Model\SchemaDefinition\JsonSchema;
use PHPModelGenerator\Model\Validator;
use PHPModelGenerator\Model\Validator\PropertyValidatorInterface;
use PHPModelGenerator\PropertyProcessor\Decorator\Property\PropertyDecoratorInterface;
use PHPModelGenerator\PropertyProcessor\Decorator\TypeHint\TypeHintDecoratorInterface;

/**
 * Class Property
 *
 * @package PHPModelGenerator\Model\Property
 */
class Property extends AbstractProperty
{
    /** @var PropertyType|null */
    protected $outputType;
    /** @var bool */
    protected $isPropertyRequired = true;
    /** @var bool */
    protected $isPropertyReadOnly = false;
    /** @var bool */
    protected $isPropertyInternal = false;
    /** @var mixed */
    protected $defaultValue;

    /** @var Validator[] */
    protected $validators = [];
    /** @var Schema */
    protected $nestedSchema;
    /** @var PropertyDecoratorInterface[] */
    public $decorators = [];
    /** @var TypeHintDecoratorInterface[] */
    public $typeHintDecorators = [];

    private array $renderedTypeHints = [];
    /** Track the amount of unresolved validators */
    private int $pendingValidators = 0;

    /**
     * Property constructor.
     *
     * @throws SchemaException
     */
    public function __construct(
        string $name,
        protected ?PropertyType $type,
        JsonSchema $jsonSchema,
        protected string $description = '',
    ) {
        parent::__construct($name, $jsonSchema);

        $this->resolve();
    }

    /**
     * @inheritdoc
     */
    public function getType(bool $outputType = false): ?PropertyType
    {
        // If the output type differs from an input type also accept the output type
        // (in this case the transforming filter is skipped)
        // TODO: PHP 8 use union types to accept multiple input types
        if (!$outputType
            && $this->type
            && $this->outputType
            && $this->outputType->getName() !== $this->type->getName()
        ) {
            return null;
        }

        return $outputType && $this->outputType !== null ? $this->outputType : $this->type;
    }

    /**
     * @inheritdoc
     */
    public function setType(
        ?PropertyType $type = null,
        ?PropertyType $outputType = null,
        bool $reset = false,
    ): PropertyInterface {
        if ($reset) {
            $this->typeHintDecorators = [];
        }

        $this->type = $type;
        $this->outputType = $outputType;

        return $this;
    }

    /**
     * @inheritdoc
     */
    public function getTypeHint(bool $outputType = false, array $skipDecorators = []): string
    {
        if (isset($this->renderedTypeHints[$outputType])) {
            return $this->renderedTypeHints[$outputType];
        }

        static $skipDec = [];

        $additionalSkips = array_diff($skipDecorators, $skipDec);
        $skipDec = array_merge($skipDec, $additionalSkips);

        $input = [$outputType && $this->outputType !== null ? $this->outputType : $this->type];

        // If the output type differs from an input type also accept the output type
        if (!$outputType && $this->outputType !== null && $this->outputType !== $this->type) {
            $input = [$this->type, $this->outputType];
        }

        $input = join(
            '|',
            array_filter(array_map(function (?PropertyType $input) use ($outputType, $skipDec): string {
                $typeHint = $input ? $input->getName() : '';

                $filteredDecorators = array_filter(
                    $this->typeHintDecorators,
                    static fn(TypeHintDecoratorInterface $decorator): bool => !in_array($decorator::class, $skipDec),
                );

                foreach ($filteredDecorators as $decorator) {
                    $typeHint = $decorator->decorate($typeHint, $outputType);
                }

                return $typeHint;
            }, $input)),
        );

        $skipDec = array_diff($skipDec, $additionalSkips);

        return $this->renderedTypeHints[$outputType] = $input ?: 'mixed';
    }

    /**
     * @inheritdoc
     */
    public function addTypeHintDecorator(TypeHintDecoratorInterface $typeHintDecorator): PropertyInterface
    {
        $this->typeHintDecorators[] = $typeHintDecorator;

        return $this;
    }

    /**
     * @inheritdoc
     */
    public function getDescription(): string
    {
        return $this->description;
    }

    /**
     * @inheritdoc
     */
    public function addValidator(PropertyValidatorInterface $validator, int $priority = 99): PropertyInterface
    {
        if (!$validator->isResolved()) {
            $this->isResolved = false;

            $this->pendingValidators++;

            $validator->onResolve(function (): void {
                if (--$this->pendingValidators === 0) {
                    $this->resolve();
                }
            });
        }

        $this->validators[] = new Validator($validator, $priority);

        return $this;
    }

    /**
     * @inheritdoc
     */
    public function getValidators(): array
    {
        return $this->validators;
    }

    /**
     * @inheritdoc
     */
    public function filterValidators(callable $filter): PropertyInterface
    {
        $this->validators = array_filter($this->validators, $filter);

        return $this;
    }

    /**
     * @inheritdoc
     */
    public function getOrderedValidators(): array
    {
        usort(
            $this->validators,
            static fn(Validator $validator, Validator $comparedValidator): int =>
                $validator->getPriority() <=> $comparedValidator->getPriority(),
        );

        return array_map(
            static fn(Validator $validator): PropertyValidatorInterface => $validator->getValidator(),
            $this->validators,
        );
    }

    /**
     * @inheritdoc
     */
    public function addDecorator(PropertyDecoratorInterface $decorator): PropertyInterface
    {
        $this->decorators[] = $decorator;

        return $this;
    }

    /**
     * @inheritdoc
     */
    public function resolveDecorator(string $input, bool $nestedProperty): string
    {
        foreach ($this->decorators as $decorator) {
            $input = $decorator->decorate($input, $this, $nestedProperty);
        }

        return $input;
    }

    /**
     * @inheritdoc
     */
    public function getDecorators(): array
    {
        return $this->decorators;
    }

    /**
     * @inheritdoc
     */
    public function setRequired(bool $isPropertyRequired): PropertyInterface
    {
        $this->isPropertyRequired = $isPropertyRequired;

        return $this;
    }

    /**
     * @inheritdoc
     */
    public function setReadOnly(bool $isPropertyReadOnly): PropertyInterface
    {
        $this->isPropertyReadOnly = $isPropertyReadOnly;

        return $this;
    }

    /**
     * @inheritdoc
     */
    public function setDefaultValue($defaultValue, bool $raw = false): PropertyInterface
    {
        $this->defaultValue = $defaultValue !== null && !$raw ? var_export($defaultValue, true) : $defaultValue;

        return $this;
    }

    /**
     * @inheritdoc
     */
    public function getDefaultValue(): ?string
    {
        return $this->defaultValue;
    }

    /**
     * @inheritdoc
     */
    public function isRequired(): bool
    {
        return $this->isPropertyRequired;
    }

    /**
     * @inheritdoc
     */
    public function isReadOnly(): bool
    {
        return $this->isPropertyReadOnly;
    }

    /**
     * @inheritdoc
     */
    public function setNestedSchema(Schema $schema): PropertyInterface
    {
        $this->nestedSchema = $schema;
        return $this;
    }

    /**
     * @inheritdoc
     */
    public function getNestedSchema(): ?Schema
    {
        return $this->nestedSchema;
    }

    /**
     * @inheritdoc
     */
    public function setInternal(bool $isPropertyInternal): PropertyInterface
    {
        $this->isPropertyInternal = $isPropertyInternal;
        return $this;
    }

    /**
     * @inheritdoc
     */
    public function isInternal(): bool
    {
        return $this->isPropertyInternal;
    }
}