Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
95.52% covered (success)
95.52%
64 / 67
85.71% covered (warning)
85.71%
12 / 14
CRAP
0.00% covered (danger)
0.00%
0 / 1
PublicProperty
95.52% covered (success)
95.52%
64 / 67
85.71% covered (warning)
85.71%
12 / 14
50
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
11
 findPromotedProperty
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
8
 hasDefaultValue
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
8
 getDefaultValue
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 allowsNull
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 isRequired
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isField
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 appliesToContext
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getFieldPriority
60.00% covered (warning)
60.00%
3 / 5
0.00% covered (danger)
0.00%
0 / 1
2.26
 getValue
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setValue
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
5
 markValueAsMissing
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getTypehint
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getAttributes
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
6
1<?php
2namespace Apie\Core\Metadata\Fields;
3
4use Apie\Core\Attributes\ColumnPriority;
5use Apie\Core\Attributes\Optional;
6use Apie\Core\Attributes\RuntimeCheck;
7use Apie\Core\Context\ApieContext;
8use Apie\Core\Enums\DoNotChangeUploadedFile;
9use Apie\Core\Metadata\GetterInterface;
10use Apie\Core\Metadata\SetterInterface;
11use Apie\Core\Utils\ConverterUtils;
12use Apie\TypeConverter\ReflectionTypeFactory;
13use ReflectionProperty;
14use ReflectionType;
15
16final class PublicProperty implements FieldWithPossibleDefaultValue, GetterInterface, SetterInterface
17{
18    private bool $required;
19
20    private bool $field;
21
22    private bool $defaultValueAvailable;
23    private mixed $defaultValue;
24
25    public function __construct(
26        private readonly ReflectionProperty $property,
27        bool $optional = false,
28        private bool $setterHooks = false,
29    ) {
30        $hasDefaultValue = $this->hasDefaultValue();
31        $this->field = 'never' !== (string) $this->property->getType();
32        if (PHP_VERSION_ID >= 80400) {
33            if ($this->setterHooks) {
34                $settableType = $this->property->getSettableType();
35                if ('never' === (string) $settableType) {
36                    $this->field = false;
37                }
38            } elseif (null === $this->property->getHook(\PropertyHookType::Get)
39                && null !== $this->property->getHook(\PropertyHookType::Set)
40                && $this->property->isVirtual()) {
41                $this->field = false;
42            }
43        }
44
45        $this->required = !$optional
46            && empty($property->getAttributes(Optional::class))
47            && empty($property->getAttributes(RuntimeCheck::class))
48            && !$hasDefaultValue
49            && $this->field;
50    }
51
52    /**
53     * ReflectionProperty::hasDefaultValue() returns false for promoted public properties,
54     * so we look up the constructors to find ReflectionParameter and return this.
55     * @param \ReflectionClass<object> $class
56     */
57    private function findPromotedProperty(\ReflectionClass $class): ?\ReflectionParameter
58    {
59        foreach ($class->getConstructor()?->getParameters() ?? [] as $parameter) {
60            if ($parameter->isPromoted()
61                && $parameter->isDefaultValueAvailable()
62                && $parameter->name === $this->property->name
63            ) {
64                return $parameter;
65            }
66        }
67        if (!$this->property->isPrivate()) {
68            $parentClass = $class->getConstructor()?->getDeclaringClass()->getParentClass();
69            if ($parentClass && $parentClass->name !== $class->name) {
70                return $this->findPromotedProperty($parentClass);
71            }
72        }
73        return null;
74    }
75
76    public function hasDefaultValue(): bool
77    {
78        if (!isset($this->defaultValueAvailable)) {
79            $this->defaultValueAvailable = $this->property->hasDefaultValue();
80            // if there is no typehint hasDefaultValue() always returns true
81            if (null === $this->property->getType()) {
82                $this->defaultValueAvailable = $this->property->getDefaultValue() !== null;
83            }
84            if ($this->defaultValueAvailable) {
85                $this->defaultValue = $this->property->getDefaultValue();
86            } elseif ($this->setterHooks && $this->property->isPromoted()) {
87                $argument = $this->findPromotedProperty($this->property->getDeclaringClass());
88                if ($argument && $argument->isDefaultValueAvailable()) {
89                    $this->defaultValueAvailable = true;
90                    $this->defaultValue = $argument->getDefaultValue();
91                }
92            }
93
94        }
95        return $this->defaultValueAvailable;
96    }
97
98    public function getDefaultValue(): mixed
99    {
100        $this->hasDefaultValue();
101        return $this->defaultValue;
102    }
103
104    public function allowsNull(): bool
105    {
106        $type = $this->getTypehint();
107        return $type === null || $type->allowsNull();
108    }
109
110    public function isRequired(): bool
111    {
112        return $this->required;
113    }
114
115    public function isField(): bool
116    {
117        return $this->field;
118    }
119
120    public function appliesToContext(ApieContext $apieContext): bool
121    {
122        return $apieContext->appliesToContext($this->property);
123    }
124
125    public function getFieldPriority(): ?int
126    {
127        $attributes = $this->property->getAttributes(ColumnPriority::class);
128        if (empty($attributes)) {
129            return null;
130        }
131
132        $attribute = reset($attributes);
133        return $attribute->newInstance()->priority;
134    }
135
136    public function getValue(object $object, ApieContext $apieContext): mixed
137    {
138        return $this->property->getValue($object);
139    }
140
141    public function setValue(object $object, mixed $value, ApieContext $apieContext): void
142    {
143        if ($value !== DoNotChangeUploadedFile::DoNotChange && $this->field) {
144            if (!$this->property->isInitialized($object) || !$this->property->isReadOnly()) {
145                $this->property->setValue($object, $value);
146            }
147        }
148    }
149
150    public function markValueAsMissing(): void
151    {
152    }
153
154    public function getTypehint(): ?ReflectionType
155    {
156        if (!$this->field) {
157            return ReflectionTypeFactory::createReflectionType('never');
158        }
159        return $this->property->getType();
160    }
161
162    public function getAttributes(string $attributeClass, bool $classDocBlock = true, bool $propertyDocblock = true, bool $argumentDocBlock = true): array
163    {
164        $list = [];
165        if ($propertyDocblock) {
166            foreach ($this->property->getAttributes($attributeClass, \ReflectionAttribute::IS_INSTANCEOF) as $attribute) {
167                $list[] = $attribute->newInstance();
168            }
169        }
170        $class = ConverterUtils::toReflectionClass($this->property);
171        if ($class && $classDocBlock) {
172            foreach ($class->getAttributes($attributeClass, \ReflectionAttribute::IS_INSTANCEOF) as $attribute) {
173                $list[] = $attribute->newInstance();
174            }
175        }
176        return $list;
177    }
178}