99use Yiisoft \Validator \AttributeTranslatorProviderInterface ;
1010use Yiisoft \Validator \DataSetInterface ;
1111use Yiisoft \Validator \Helper \ObjectParser ;
12+ use Yiisoft \Validator \RuleInterface ;
13+ use Yiisoft \Validator \RulesProvider \AttributesRulesProvider ;
1214use Yiisoft \Validator \RulesProviderInterface ;
1315
1416/**
15- * This data set makes use of attributes introduced in PHP 8. It simplifies rules configuration process, especially for
16- * nested data and relations. Please refer to the guide for examples.
17+ * A data set for object data. The object passed to this data set can provide rules and data by implementing
18+ * {@see RuleInterface} 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 RuleInterface}:
22+ *
23+ * ```php
24+ * final class Author implements RulesProviderInterface
25+ * {
26+ * public function getRules(): iterable
27+ * {
28+ * return ['age' => [new Number(min: 18)]];
29+ * }
30+ * }
31+ * ```
32+ *
33+ * 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+ * return $this->getData()[$attribute] ?? null;
41+ * }
42+ *
43+ * public function getData(): mixed
44+ * {
45+ * return ['name' => 'John', 'age' => 18];
46+ * }
47+ *
48+ * public function hasAttribute(string $attribute): bool
49+ * {
50+ * return array_key_exists($attribute, $this->getData());
51+ * }
52+ * }
53+ * ```
54+ *
55+ * 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+ * This way the validated structures can be presented as DTO classes with references to each other.
59+ *
60+ * An example of DTO with both one-to-one (requires PHP > 8.0) and one-to-many (requires PHP > 8.1) relations:
61+ *
62+ * ```php
63+ * final class Post
64+ * {
65+ * #[HasLength(max: 255)]
66+ * public string $title = '';
67+ *
68+ * #[Nested]
69+ * public Author|null $author = null;
70+ *
71+ * // Passing instances is available only since PHP 8.1.
72+ * #[Each(new Nested([File::class])]
73+ * public array $files = [];
74+ *
75+ * public function __construct()
76+ * {
77+ * $this->author = new Author();
78+ * }
79+ * }
80+ *
81+ * final class Author
82+ * {
83+ * #[HasLength(min: 1)]
84+ * public string $name = '';
85+ * }
86+ *
87+ * // Some rules, like "Nested" can be also configured through the class attribute.
88+ *
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 the highest priority over attributes and properties so, when using
136+ * together, the latter ones will be ignored without exception.
137+ *
138+ * When {@see RuleInterface} / {@see DataSetInterface} are not implemented, uses {@see ObjectParser} and supports
139+ * 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 string, use {@see AttributesRulesProvider} instead.
17142 *
18143 * @link https://www.php.net/manual/en/language.attributes.overview.php
19144 */
20145final class ObjectDataSet implements RulesProviderInterface, DataSetInterface, AttributeTranslatorProviderInterface
21146{
147+ /**
148+ * @var bool Whether an {@see $object} provided a data set by implementing {@see DataSetInterface}.
149+ */
22150 private bool $ dataSetProvided ;
151+ /**
152+ * @var bool Whether an {@see $object} provided rules by implementing {@see RulesProviderInterface}.
153+ */
23154 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+ */
24159 private ObjectParser $ parser ;
25160
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+ */
26167 public function __construct (
168+ /**
169+ * @var object An object containing rules and data.
170+ */
27171 private object $ object ,
28172 int $ propertyVisibility = ReflectionProperty::IS_PRIVATE |
29173 ReflectionProperty::IS_PROTECTED |
30174 ReflectionProperty::IS_PUBLIC ,
31- bool $ useCache = true
175+ bool $ useCache = true ,
32176 ) {
33177 $ this ->dataSetProvided = $ this ->object instanceof DataSetInterface;
34178 $ this ->rulesProvided = $ this ->object instanceof RulesProviderInterface;
@@ -39,6 +183,21 @@ public function __construct(
39183 );
40184 }
41185
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 the highest priority over
190+ * attributes, so, when using 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+ */
42201 public function getRules (): iterable
43202 {
44203 if ($ this ->rulesProvided ) {
@@ -48,15 +207,25 @@ public function getRules(): iterable
48207 return $ object ->getRules ();
49208 }
50209
51- // Providing data set assumes object has its own attributes and rules getting logic. So further parsing of
52- // Reflection properties and rules is skipped intentionally.
210+ // Providing data set assumes object has its own rules getting logic. So further parsing of rules is skipped
211+ // intentionally.
53212 if ($ this ->dataSetProvided ) {
54213 return [];
55214 }
56215
57216 return $ this ->parser ->getRules ();
58217 }
59218
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+ */
60229 public function getAttributeValue (string $ attribute ): mixed
61230 {
62231 if ($ this ->dataSetProvided ) {
@@ -68,6 +237,14 @@ public function getAttributeValue(string $attribute): mixed
68237 return $ this ->parser ->getAttributeValue ($ attribute );
69238 }
70239
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+ */
71248 public function hasAttribute (string $ attribute ): bool
72249 {
73250 if ($ this ->dataSetProvided ) {
@@ -79,6 +256,12 @@ public function hasAttribute(string $attribute): bool
79256 return $ this ->parser ->hasAttribute ($ attribute );
80257 }
81258
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+ */
82265 public function getData (): mixed
83266 {
84267 if ($ this ->dataSetProvided ) {
@@ -90,6 +273,12 @@ public function getData(): mixed
90273 return $ this ->parser ->getData ();
91274 }
92275
276+ /**
277+ * An optional attribute names translator. It's taken from the {@see $object} when
278+ * {@see AttributeTranslatorProviderInterface} is implemented. In case of it's missing, a `null` value is returned.
279+ *
280+ * @return AttributeTranslatorInterface|null An attribute translator instance or `null if it was not provided.
281+ */
93282 public function getAttributeTranslator (): ?AttributeTranslatorInterface
94283 {
95284 return $ this ->parser ->getAttributeTranslator ();
0 commit comments