Passed
Push — master ( b674ac...2bc77e )
by Kirill
04:21
created

Instantiator::validateArgumentPosition()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 10
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
eloc 5
nc 3
nop 2
dl 0
loc 10
rs 10
c 1
b 0
f 0
1
<?php
2
3
/**
4
 * This file is part of Attributes package.
5
 *
6
 * For the full copyright and license information, please view the LICENSE
7
 * file that was distributed with this source code.
8
 */
9
10
declare(strict_types=1);
11
12
namespace Spiral\Attributes\Internal;
13
14
use Doctrine\Common\Annotations\DocParser;
15
use Spiral\Attributes\Exception\AttributeException;
16
17
/**
18
 * @internal Instantiator is an internal library class, please do not use it in your code.
19
 * @psalm-internal Spiral\Attributes
20
 */
21
class Instantiator
22
{
23
    /**
24
     * An error message that occurs when the attribute has no public field in
25
     * a format compatible with doctrine/annotations.
26
     *
27
     * @see DocParser::Annotation()
28
     *
29
     * @var string
30
     */
31
    private const ERROR_INVALID_PROPERTY =
32
        'The attribute #[%s] declared on %s does not have a property named "%s".' . "\n" .
33
        'Available properties: %s'
34
    ;
35
36
    /**
37
     * An error that occurs when specifying invalid arguments for an attribute
38
     * in a format compatible with doctrine/annotations.
39
     *
40
     * @see DocParser::syntaxError()
41
     *
42
     * @var string
43
     */
44
    private const ERROR_INVALID_ARGUMENT = 'Expected %s, got %s';
45
46
    /**
47
     * @var string
48
     */
49
    private const CONSTRUCTOR_NAME = '__construct';
50
51
    /**
52
     * @var string
53
     */
54
    private const DEFAULT_PROPERTY_NAME = 'value';
55
56
    /**
57
     * @param \ReflectionClass $attribute
58
     * @param array $arguments
59
     * @param string $context
60
     * @return object
61
     * @throws \ReflectionException
62
     */
63
    public function instantiate(\ReflectionClass $attribute, array $arguments, string $context): object
64
    {
65
        $arguments = $this->formatArguments($arguments);
66
67
        // Using constructor
68
        if ($this->getConstructor($attribute)) {
69
            return $attribute->newInstance($arguments);
70
        }
71
72
        // Using direct insert
73
        $instance = $attribute->newInstanceWithoutConstructor();
74
75
        foreach ($arguments as $name => $value) {
76
            try {
77
                $property = $attribute->getProperty($name);
78
79
                if (! $property->isPublic()) {
80
                    throw $this->propertyNotFound($attribute, $name, $context);
81
                }
82
83
                $instance->$name = $value;
84
            } catch (\Throwable $e) {
85
                throw $this->propertyNotFound($attribute, $name, $context);
86
            }
87
        }
88
89
        return $instance;
90
    }
91
92
    /**
93
     * @param iterable $arguments
94
     * @return array
95
     */
96
    private function formatArguments(iterable $arguments): array
97
    {
98
        $result = [];
99
100
        foreach ($arguments as $name => $value) {
101
            if (\is_int($name)) {
102
                $this->validateArgumentPosition($name, $value);
103
104
                $name = self::DEFAULT_PROPERTY_NAME;
105
            }
106
107
            $result[$name] = $value;
108
        }
109
110
        return $result;
111
    }
112
113
    /**
114
     * @param int $index
115
     * @param mixed $value
116
     */
117
    private function validateArgumentPosition(int $index, $value): void
118
    {
119
        if ($index === 0) {
120
            return;
121
        }
122
123
        $value = \is_scalar($value) ? \var_export($value, true) : \get_debug_type($value);
124
        $message = \sprintf(self::ERROR_INVALID_ARGUMENT, self::DEFAULT_PROPERTY_NAME, $value);
125
126
        throw AttributeException::syntaxError($message);
127
    }
128
129
    /**
130
     * @param \ReflectionClass $attr
131
     * @param string $name
132
     * @param string $context
133
     * @return AttributeException
134
     */
135
    private function propertyNotFound(\ReflectionClass $attr, string $name, string $context): AttributeException
136
    {
137
        $available = $this->getAvailablePropertiesString($attr);
138
        $message = \sprintf(self::ERROR_INVALID_PROPERTY, $attr->getName(), $context, $name, $available);
139
140
        return AttributeException::creationError($message);
141
    }
142
143
    /**
144
     * @param \ReflectionClass $class
145
     * @return string
146
     */
147
    private function getAvailablePropertiesString(\ReflectionClass $class): string
148
    {
149
        return \implode(', ', \get_class_vars($class->getName()));
150
    }
151
152
    /**
153
     * @param \ReflectionMethod $construct
154
     * @return string
155
     */
156
    private function getAvailableNamedPropertiesString(\ReflectionMethod $construct): string
0 ignored issues
show
Unused Code introduced by
The method getAvailableNamedPropertiesString() is not used, and could be removed.

This check looks for private methods that have been defined, but are not used inside the class.

Loading history...
157
    {
158
        $names = [];
159
160
        foreach ($construct->getParameters() as $parameter) {
161
            $names[] = $parameter->getName();
162
        }
163
164
        return \implode(', ', $names);
165
    }
166
167
    /**
168
     * @param \ReflectionClass $class
169
     * @return bool
170
     */
171
    protected function hasConstructor(\ReflectionClass $class): bool
172
    {
173
        return $this->getConstructor($class) !== null;
174
    }
175
176
    /**
177
     * @param \ReflectionClass $class
178
     * @return \ReflectionMethod|null
179
     */
180
    protected function getConstructor(\ReflectionClass $class): ?\ReflectionMethod
181
    {
182
        if ($class->hasMethod(self::CONSTRUCTOR_NAME)) {
183
            return $class->getMethod(self::CONSTRUCTOR_NAME);
184
        }
185
186
        if ($constructor = $this->getTraitConstructors($class)) {
187
            return $constructor;
188
        }
189
190
        if ($parent = $class->getParentClass()) {
191
            return $this->getConstructor($parent);
192
        }
193
194
        return null;
195
    }
196
197
    /**
198
     * @param \ReflectionClass $class
199
     * @return \ReflectionMethod|null
200
     */
201
    private function getTraitConstructors(\ReflectionClass $class): ?\ReflectionMethod
202
    {
203
        foreach ($class->getTraits() as $trait) {
204
            if ($constructor = $this->getConstructor($trait)) {
205
                return $constructor;
206
            }
207
208
            if ($constructor = $this->getTraitConstructors($trait)) {
209
                return $constructor;
210
            }
211
        }
212
213
        return null;
214
    }
215
}