Passed
Pull Request — master (#464)
by Sergei
02:47
created

ObjectDataSet   A

Complexity

Total Complexity 12

Size/Duplication

Total Lines 145
Duplicated Lines 0 %

Test Coverage

Coverage 0%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 30
dl 0
loc 145
ccs 0
cts 0
cp 0
rs 10
c 1
b 0
f 0
wmc 12

7 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
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
    public function __construct(
169
        /**
170
         * @var object An object containing rules and data.
171
         */
172
        private object $object,
173
        int $propertyVisibility = ReflectionProperty::IS_PRIVATE |
174
        ReflectionProperty::IS_PROTECTED |
175
        ReflectionProperty::IS_PUBLIC,
176
        bool $useCache = true,
177
    ) {
178
        $this->dataSetProvided = $this->object instanceof DataSetInterface;
179
        $this->rulesProvided = $this->object instanceof RulesProviderInterface;
180
        $this->parser = new ObjectParser(
181
            source: $object,
182
            propertyVisibility: $propertyVisibility,
183
            useCache: $useCache
184
        );
185
    }
186
187
    /**
188
     * Returns {@see $object} rules specified via {@see RulesProviderInterface::getRules()} implementation or parsed
189
     * from attributes attached to class properties and class itself. For the latter case repetitive calls utilize cache
190
     * if it's enabled in {@see $useCache}. Rules provided via separate method have a higher priority over attributes,
191
     * so, when used together, the latter ones will be ignored without exception.
192
     *
193
     * @return iterable The resulting rules is an array with the following structure:
194
     *
195
     * ```php
196
     * [
197
     *     [new AtLeast(['name', 'author'])], // Rules not bound to a specific attribute.
198
     *     'files' => [new Count(max: 3)], // Attribute specific rules.
199
     * ],
200
     * ```
201
     */
202
    public function getRules(): iterable
203
    {
204
        if ($this->rulesProvided) {
205
            /** @var RulesProviderInterface $object */
206
            $object = $this->object;
207
208
            return $object->getRules();
209
        }
210
211
        // Providing data set assumes object has its own rules getting logic. So further parsing of rules is skipped
212
        // intentionally.
213
        if ($this->dataSetProvided) {
214
            return [];
215
        }
216
217
        return $this->parser->getRules();
218
    }
219
220
    /**
221
     * Returns an attribute value by its name.
222
     *
223
     * Note that in case of non-existing attribute a default `null` value is returned. If you need to check the presence
224
     * of attribute or return a different default value, use {@see hasAttribute()} instead.
225
     *
226
     * @param string $attribute Attribute name.
227
     *
228
     * @return mixed Attribute value.
229
     */
230
    public function getAttributeValue(string $attribute): mixed
231
    {
232
        if ($this->dataSetProvided) {
233
            /** @var DataSetInterface $object */
234
            $object = $this->object;
235
            return $object->getAttributeValue($attribute);
236
        }
237
238
        return $this->parser->getAttributeValue($attribute);
239
    }
240
241
    /**
242
     * Whether this data set has the attribute with a given name. Note that this means existence only and attributes
243
     * with empty values are treated as present too.
244
     *
245
     * @param string $attribute Attribute name.
246
     *
247
     * @return bool Whether the attribute exists: `true` - exists and `false` - otherwise.
248
     */
249
    public function hasAttribute(string $attribute): bool
250
    {
251
        if ($this->dataSetProvided) {
252
            /** @var DataSetInterface $object */
253
            $object = $this->object;
254
            return $object->hasAttribute($attribute);
255
        }
256
257
        return $this->parser->hasAttribute($attribute);
258
    }
259
260
    /**
261
     * Returns the validated data as a whole.
262
     *
263
     * @return array|null Validated data, has mixed type if it was provided via {@see DataSetInterface::getData()}
264
     * implementation, otherwise it's always an associative array - a mapping between property names and their values.
265
     */
266
    public function getData(): ?array
267
    {
268
        if ($this->dataSetProvided) {
269
            /** @var DataSetInterface $object */
270
            $object = $this->object;
271
            return $object->getData();
272
        }
273
274
        return $this->parser->getData();
275
    }
276
277
    public function getSource(): object
278
    {
279
        return $this->object;
280
    }
281
282
    /**
283
     * An optional attribute names translator. It's taken from the {@see $object} when
284
     * {@see AttributeTranslatorProviderInterface} is implemented. In case of it's missing, a `null` value is returned.
285
     *
286
     * @return AttributeTranslatorInterface|null An attribute translator instance or `null` if it was not provided.
287
     */
288
    public function getAttributeTranslator(): ?AttributeTranslatorInterface
289
    {
290
        return $this->parser->getAttributeTranslator();
291
    }
292
}
293