Passed
Pull Request — master (#464)
by Alexander
02:35
created

ObjectDataSet::getSource()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
ccs 0
cts 0
cp 0
crap 2
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Validator\DataSet;
6
7
use ReflectionProperty;
8
use Yiisoft\Validator\AttributeTranslatorInterface;
9
use Yiisoft\Validator\AttributeTranslatorProviderInterface;
10
use Yiisoft\Validator\DataSetInterface;
11
use Yiisoft\Validator\DataWrapperInterface;
12
use Yiisoft\Validator\Helper\ObjectParser;
13
use Yiisoft\Validator\RulesProvider\AttributesRulesProvider;
14
use Yiisoft\Validator\RulesProviderInterface;
15
16
/**
17
 * A data set for object data. The object passed to this data set can provide rules and data by implementing
18
 * {@see RulesProviderInterface} and {@see DataSetInterface}. Alternatively this data set allows getting rules from PHP
19
 * attributes (attached to class properties and class itself) and data from object properties.
20
 *
21
 * An example of object implementing {@see RulesProviderInterface}:
22
 *
23
 * ```php
24 59
 * final class Author implements RulesProviderInterface
25
 * {
26
 *     public function getRules(): iterable
27
 *     {
28
 *         return ['age' => [new Number(min: 18)]];
29
 *     }
30
 * }
31 59
 * ```
32 59
 *
33 59
 * An example of object implementing {@see DataSetInterface}:
34
 *
35
 * ```php
36
 * final class Author implements DataSetInterface
37
 * {
38
 *     public function getAttributeValue(string $attribute): mixed
39
 *     {
40 55
 *         return $this->getData()[$attribute] ?? null;
41
 *     }
42 55
 *
43
 *     public function getData(): mixed
44 11
 *     {
45
 *         return ['name' => 'John', 'age' => 18];
46 11
 *     }
47
 *
48
 *     public function hasAttribute(string $attribute): bool
49
 *     {
50
 *         return array_key_exists($attribute, $this->getData());
51 44
 *     }
52 5
 * }
53
 * ```
54
 *
55 39
 * These two can be combined and used at the same time.
56
 *
57
 * The attributes introduced in PHP 8 simplify rules' configuration process, especially for nested data and relations.
58 48
 * This way the validated structures can be presented as DTO classes with references to each other.
59
 *
60 48
 * An example of DTO with both one-to-one (requires PHP > 8.0) and one-to-many (requires PHP > 8.1) relations:
61
 *
62 8
 * ```php
63 8
 * final class Post
64
 * {
65
 *     #[HasLength(max: 255)]
66 40
 *     public string $title = '';
67
 *
68
 *     #[Nested]
69 31
 *     public Author|null $author = null;
70
 *
71 31
 *     // Passing instances is available only since PHP 8.1.
72
 *     #[Each(new Nested([File::class])]
73 1
 *     public array $files = [];
74 1
 *
75
 *     public function __construct()
76
 *     {
77 30
 *         $this->author = new Author();
78
 *     }
79
 * }
80 36
 *
81
 * final class Author
82 36
 * {
83
 *     #[HasLength(min: 1)]
84 13
 *     public string $name = '';
85 13
 * }
86
 *
87
 * // Some rules, like "Nested" can be also configured through the class attribute.
88 23
 *
89
 * #[Nested(['url' => new Url()])]
90
 * final class File
91
 * {
92
 *     public string $url = '';
93
 * }
94
 *
95
 * $post = new Post(title: 'Yii3 Overview 3', author: 'Dmitriy');
96
 * $parser = new ObjectParser($post);
97
 * $rules = $parser->getRules();
98
 * $data = $parser->getData();
99
 * ```
100
 *
101
 * The `$rules` will contain:
102
 *
103
 * ```
104
 * [
105
 *     new Nested([
106
 *         'title' => [new HasLength(max: 255)],
107
 *         'author' => new Nested([
108
 *             'name' => [new HasLength(min: 1)],
109
 *         ]),
110
 *         'files' => new Each(new Nested([
111
 *             'url' => [new Url()],
112
 *         ])),
113
 *     ]);
114
 * ];
115
 * ```
116
 *
117
 * And the result of `$data` will be:
118
 *
119
 * ```php
120
 * [
121
 *     'title' => 'Yii3 Overview 3',
122
 *     'author' => 'John',
123
 *     'files' => [],
124
 * ];
125
 * ```
126
 *
127
 * Note that the rule attributes can be combined with others without affecting parsing. Which properties to parse can be
128
 * configured via {@see ObjectDataSet::$propertyVisibility} and {@see ObjectDataSet::$skipStaticProperties} options.
129
 *
130
 * The other combinations of rules / data are also possible, for example: the data is provided by implementing
131
 * {@see DataSetInterface} and rules are parsed from the attributes.
132
 *
133
 * Please refer to the guide for more examples.
134
 *
135
 * Rules and data provided via separate methods have a higher priority over attributes and properties, so, when used
136
 * together, the latter ones will be ignored without exception.
137
 *
138
 * When {@see RulesProviderInterface} / {@see DataSetInterface} are not implemented, uses {@see ObjectParser} and
139
 * supports caching for data and attribute methods (partially) and rules (completely) which can be disabled on demand.
140
 *
141
 * For getting only rules by a class name string or to be able to skip static properties, use
142
 * {@see AttributesRulesProvider} instead.
143
 *
144
 * @link https://www.php.net/manual/en/language.attributes.overview.php
145
 */
146
final class ObjectDataSet implements RulesProviderInterface, DataWrapperInterface, AttributeTranslatorProviderInterface
147
{
148
    /**
149
     * @var bool Whether an {@see $object} provided a data set by implementing {@see DataSetInterface}.
150
     */
151
    private bool $dataSetProvided;
152
    /**
153
     * @var bool Whether an {@see $object} provided rules by implementing {@see RulesProviderInterface}.
154
     */
155
    private bool $rulesProvided;
156
    /**
157
     * @var ObjectParser An object parser instance used to parse rules and data from attributes if these were not
158
     * provided by implementing {@see RulesProviderInterface} and {@see DataSetInterface} accordingly.
159
     */
160
    private ObjectParser $parser;
161
162
    /**
163
     * @param int $propertyVisibility Visibility levels the properties with rules / data must have. For example: public
164
     * and protected only, this means that the rest (private ones) will be skipped. Defaults to all visibility levels
165
     * (public, protected and private).
166
     * @param bool $useCache Whether to use cache for data and attribute methods (partially) and rules (completely).
167
     *
168
     * @psalm-param int-mask-of<ReflectionProperty::IS_*> $propertyVisibility
169
     */
170
    public function __construct(
171
        /**
172
         * @var object An object containing rules and data.
173
         */
174
        private object $object,
175
        int $propertyVisibility = ReflectionProperty::IS_PRIVATE |
176
        ReflectionProperty::IS_PROTECTED |
177
        ReflectionProperty::IS_PUBLIC,
178
        bool $useCache = true,
179
    ) {
180
        $this->dataSetProvided = $this->object instanceof DataSetInterface;
181
        $this->rulesProvided = $this->object instanceof RulesProviderInterface;
182
        $this->parser = new ObjectParser(
183
            source: $object,
184
            propertyVisibility: $propertyVisibility,
185
            useCache: $useCache
186
        );
187
    }
188
189
    /**
190
     * Returns {@see $object} rules specified via {@see RulesProviderInterface::getRules()} implementation or parsed
191
     * from attributes attached to class properties and class itself. For the latter case repetitive calls utilize cache
192
     * if it's enabled in {@see $useCache}. Rules provided via separate method have a higher priority over attributes,
193
     * so, when used together, the latter ones will be ignored without exception.
194
     *
195
     * @return iterable The resulting rules is an array with the following structure:
196
     *
197
     * ```php
198
     * [
199
     *     [new AtLeast(['name', 'author'])], // Rules not bound to a specific attribute.
200
     *     'files' => [new Count(max: 3)], // Attribute specific rules.
201
     * ],
202
     * ```
203
     */
204
    public function getRules(): iterable
205
    {
206
        if ($this->rulesProvided) {
207
            /** @var RulesProviderInterface $object */
208
            $object = $this->object;
209
210
            return $object->getRules();
211
        }
212
213
        // Providing data set assumes object has its own rules getting logic. So further parsing of rules is skipped
214
        // intentionally.
215
        if ($this->dataSetProvided) {
216
            return [];
217
        }
218
219
        return $this->parser->getRules();
220
    }
221
222
    /**
223
     * Returns an attribute value by its name.
224
     *
225
     * Note that in case of non-existing attribute a default `null` value is returned. If you need to check the presence
226
     * of attribute or return a different default value, use {@see hasAttribute()} instead.
227
     *
228
     * @param string $attribute Attribute name.
229
     *
230
     * @return mixed Attribute value.
231
     */
232
    public function getAttributeValue(string $attribute): mixed
233
    {
234
        if ($this->dataSetProvided) {
235
            /** @var DataSetInterface $object */
236
            $object = $this->object;
237
            return $object->getAttributeValue($attribute);
238
        }
239
240
        return $this->parser->getAttributeValue($attribute);
241
    }
242
243
    /**
244
     * Whether this data set has the attribute with a given name. Note that this means existence only and attributes
245
     * with empty values are treated as present too.
246
     *
247
     * @param string $attribute Attribute name.
248
     *
249
     * @return bool Whether the attribute exists: `true` - exists and `false` - otherwise.
250
     */
251
    public function hasAttribute(string $attribute): bool
252
    {
253
        if ($this->dataSetProvided) {
254
            /** @var DataSetInterface $object */
255
            $object = $this->object;
256
            return $object->hasAttribute($attribute);
257
        }
258
259
        return $this->parser->hasAttribute($attribute);
260
    }
261
262
    /**
263
     * Returns the validated data as array.
264
     *
265
     * @return array|null Result of object {@see DataSetInterface::getData()} method, if it was implemented
266
     * {@see DataSetInterface}, otherwise returns the validated data as an associative array - a mapping between
267
     * property names and their values.
268
     */
269
    public function getData(): ?array
270
    {
271
        if ($this->dataSetProvided) {
272
            /** @var DataSetInterface $object */
273
            $object = $this->object;
274
            return $object->getData();
275
        }
276
277
        return $this->parser->getData();
278
    }
279
280
    public function getSource(): object
281
    {
282
        return $this->object;
283
    }
284
285
    /**
286
     * An optional attribute names translator. It's taken from the {@see $object} when
287
     * {@see AttributeTranslatorProviderInterface} is implemented. In case of it's missing, a `null` value is returned.
288
     *
289
     * @return AttributeTranslatorInterface|null An attribute translator instance or `null` if it was not provided.
290
     */
291
    public function getAttributeTranslator(): ?AttributeTranslatorInterface
292
    {
293
        return $this->parser->getAttributeTranslator();
294
    }
295
}
296