ObjectDataSet   A
last analyzed

Complexity

Total Complexity 14

Size/Duplication

Total Lines 161
Duplicated Lines 0 %

Test Coverage

Coverage 0%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 34
c 1
b 0
f 0
dl 0
loc 161
rs 10
ccs 0
cts 0
cp 0
wmc 14

8 Methods

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