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