Passed
Pull Request — master (#272)
by Sergei
02:55
created

FormModel::makePropertyPathString()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 15
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 5.025

Importance

Changes 0
Metric Value
cc 5
eloc 10
c 0
b 0
f 0
nc 5
nop 1
dl 0
loc 15
ccs 9
cts 10
cp 0.9
crap 5.025
rs 9.6111
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Form;
6
7
use InvalidArgumentException;
8
use ReflectionClass;
9
use ReflectionException;
10
use Yiisoft\Form\Exception\PropertyNotSupportNestedValuesException;
11
use Yiisoft\Form\Exception\StaticObjectPropertyException;
12
use Yiisoft\Form\Exception\UndefinedObjectPropertyException;
13
use Yiisoft\Form\Exception\ValueNotFoundException;
14
use Yiisoft\Hydrator\Validator\ValidatedInputTrait;
15
use Yiisoft\Strings\Inflector;
16
use Yiisoft\Strings\StringHelper;
17
18
use function array_key_exists;
19
use function array_slice;
20
use function is_array;
21
use function is_object;
22
use function str_contains;
23
use function strrchr;
24
use function substr;
25
26
/**
27
 * Form model represents an HTML form: its data, validation and presentation.
28
 */
29
abstract class FormModel implements FormModelInterface
30
{
31
    use ValidatedInputTrait;
32
33
    private const META_LABEL = 1;
34
    private const META_HINT = 2;
35
    private const META_PLACEHOLDER = 3;
36
37
    private static ?Inflector $inflector = null;
38
39
    /**
40
     * Returns the text hint for the specified attribute.
41
     *
42
     * @param string $attribute the attribute name.
43
     *
44
     * @return string the attribute hint.
45
     */
46 410
    public function getAttributeHint(string $attribute): string
47
    {
48 410
        return $this->readAttributeMetaValue(self::META_HINT, $attribute) ?? '';
49
    }
50
51
    /**
52
     * Returns the attribute hints.
53
     *
54
     * Attribute hints are mainly used for display purpose. For example, given an attribute `isPublic`, we can declare
55
     * a hint `Whether the post should be visible for not logged-in users`, which provides user-friendly description of
56
     * the attribute meaning and can be displayed to end users.
57
     *
58
     * Unlike label hint will not be generated, if its explicit declaration is omitted.
59
     *
60
     * Note, in order to inherit hints defined in the parent class, a child class needs to merge the parent hints with
61
     * child hints using functions such as `array_merge()`.
62
     *
63
     * @return array attribute hints (name => hint)
64
     *
65
     * @psalm-return array<string,string>
66
     */
67 104
    public function getAttributeHints(): array
68
    {
69 104
        return [];
70
    }
71
72
    /**
73
     * Returns the text label for the specified attribute.
74
     *
75
     * @param string $attribute The attribute name.
76
     *
77
     * @return string The attribute label.
78
     */
79 209
    public function getAttributeLabel(string $attribute): string
80
    {
81 209
        return $this->readAttributeMetaValue(self::META_LABEL, $attribute) ?? $this->generateAttributeLabel($attribute);
82
    }
83
84
    /**
85
     * Returns the attribute labels.
86
     *
87
     * Attribute labels are mainly used for display purpose. For example, given an attribute `firstName`, we can
88
     * declare a label `First Name` which is more user-friendly and can be displayed to end users.
89
     *
90
     * By default, an attribute label is generated automatically. This method allows you to
91
     * explicitly specify attribute labels.
92
     *
93
     * Note, in order to inherit labels defined in the parent class, a child class needs to merge the parent labels
94
     * with child labels using functions such as `array_merge()`.
95
     *
96
     * @return array attribute labels (name => label)
97
     *
98
     * {@see \Yiisoft\Form\FormModel::getAttributeLabel()}
99
     *
100
     * @psalm-return array<string,string>
101
     */
102 8
    public function getAttributeLabels(): array
103
    {
104 8
        return [];
105
    }
106
107
    /**
108
     * Returns the text placeholder for the specified attribute.
109
     *
110
     * @param string $attribute the attribute name.
111
     *
112
     * @return string the attribute placeholder.
113
     */
114 183
    public function getAttributePlaceholder(string $attribute): string
115
    {
116 183
        return $this->readAttributeMetaValue(self::META_PLACEHOLDER, $attribute) ?? '';
117
    }
118
119 386
    public function getAttributeValue(string $attribute): mixed
120
    {
121 386
        return $this->readAttributeValue($attribute);
122
    }
123
124
    /**
125
     * Returns the attribute placeholders.
126
     *
127
     * @return array attribute placeholder (name => placeholder)
128
     *
129
     * @psalm-return array<string,string>
130
     */
131 122
    public function getAttributePlaceholders(): array
132
    {
133 122
        return [];
134
    }
135
136
    /**
137
     * Returns the form name that this model class should use.
138
     *
139
     * The form name is mainly used by {@see \Yiisoft\Form\Helper\HtmlForm} to determine how to name the input
140
     * fields for the attributes in a model.
141
     * If the form name is "A" and an attribute name is "b", then the corresponding input name would be "A[b]".
142
     * If the form name is an empty string, then the input name would be "b".
143
     *
144
     * The purpose of the above naming schema is that for forms which contain multiple different models, the attributes
145
     * of each model are grouped in sub-arrays of the POST-data, and it is easier to differentiate between them.
146
     *
147
     * By default, this method returns the model class name (without the namespace part) as the form name. You may
148
     * override it when the model is used in different forms.
149
     *
150
     * @return string The form name of this model class.
151
     */
152 435
    public function getFormName(): string
153
    {
154 435
        if (str_contains(static::class, '@anonymous')) {
155 7
            return '';
156
        }
157
158 429
        $className = strrchr(static::class, '\\');
159 429
        if ($className === false) {
160 1
            return static::class;
161
        }
162
163 428
        return substr($className, 1);
164
    }
165
166
    /**
167
     * If there is such attribute in the set.
168
     */
169 475
    public function hasAttribute(string $attribute): bool
170
    {
171
        try {
172 475
            $this->readAttributeValue($attribute);
173 4
        } catch (ValueNotFoundException) {
174 4
            return false;
175
        }
176 473
        return true;
177
    }
178
179
    public function isValid(): bool
180
    {
181
        return (bool) $this->getValidationResult()?->isValid();
182
    }
183
184
    /**
185
     * @throws UndefinedObjectPropertyException
186
     * @throws StaticObjectPropertyException
187
     * @throws PropertyNotSupportNestedValuesException
188
     * @throws ValueNotFoundException
189
     */
190 497
    private function readAttributeValue(string $attribute): mixed
191
    {
192 497
        $path = $this->normalizePath($attribute);
193
194 497
        $value = $this;
195 497
        $keys = [[static::class, $this]];
196 497
        foreach ($path as $key) {
197 497
            $keys[] = [$key, $value];
198
199 497
            if (is_array($value)) {
200 4
                if (array_key_exists($key, $value)) {
201 2
                    $value = $value[$key];
202 2
                    continue;
203
                }
204 2
                return null;
205
            }
206
207 497
            if (is_object($value)) {
208 497
                $class = new ReflectionClass($value);
209
                try {
210 497
                    $property = $class->getProperty($key);
211 5
                } catch (ReflectionException) {
212 5
                    throw new UndefinedObjectPropertyException($this->makePropertyPathString($keys));
213
                }
214 495
                if ($property->isStatic()) {
215 1
                    throw new StaticObjectPropertyException($this->makePropertyPathString($keys));
216
                }
217 495
                if (PHP_VERSION_ID < 80100) {
218 495
                    $property->setAccessible(true);
219
                }
220 495
                $value = $property->getValue($value);
221 495
                continue;
222
            }
223
224 2
            if ($value === null) {
225 1
                return null;
226
            }
227
228 1
            array_pop($keys);
229 1
            throw new PropertyNotSupportNestedValuesException($this->makePropertyPathString($keys));
230
        }
231
232 493
        return $value;
233
    }
234
235 445
    private function readAttributeMetaValue(int $metaKey, string $attribute): ?string
236
    {
237 445
        $path = $this->normalizePath($attribute);
238
239 445
        $value = $this;
240 445
        $n = 0;
241 445
        foreach ($path as $key) {
242 445
            if ($value instanceof self) {
243 445
                $nestedAttribute = implode('.', array_slice($path, $n));
244 445
                $data = match ($metaKey) {
245 445
                    self::META_LABEL => $value->getAttributeLabels(),
246 445
                    self::META_HINT => $value->getAttributeHints(),
247 445
                    self::META_PLACEHOLDER => $value->getAttributePlaceholders(),
248 445
                    default => throw new InvalidArgumentException('Invalid meta key.'),
249 445
                };
250 445
                if (array_key_exists($nestedAttribute, $data)) {
251 214
                    return $data[$nestedAttribute];
252
                }
253
            }
254
255 351
            $class = new ReflectionClass($value);
256
            try {
257 351
                $property = $class->getProperty($key);
258 3
            } catch (ReflectionException) {
259 3
                return null;
260
            }
261 348
            if ($property->isStatic()) {
262
                return null;
263
            }
264
265 348
            if (PHP_VERSION_ID < 80100) {
266 348
                $property->setAccessible(true);
267
            }
268
            /** @var mixed $value */
269 348
            $value = $property->getValue($value);
270 348
            if (!is_object($value)) {
271 344
                return null;
272
            }
273
274 4
            $n++;
275
        }
276
277 1
        return null;
278
    }
279
280
    /**
281
     * Generates a user-friendly attribute label based on the give attribute name.
282
     *
283
     * This is done by replacing underscores, dashes and dots with blanks and changing the first letter of each word to
284
     * upper case.
285
     *
286
     * For example, 'department_name' or 'DepartmentName' will generate 'Department Name'.
287
     *
288
     * @param string $attribute The attribute name.
289
     *
290
     * @return string The attribute label.
291
     */
292 28
    private function generateAttributeLabel(string $attribute): string
293
    {
294 28
        if (self::$inflector === null) {
295 1
            self::$inflector = new Inflector();
296
        }
297
298 28
        return StringHelper::uppercaseFirstCharacterInEachWord(
299 28
            self::$inflector->toWords($attribute)
0 ignored issues
show
Bug introduced by
The method toWords() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

299
            self::$inflector->/** @scrutinizer ignore-call */ 
300
                              toWords($attribute)

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
300 28
        );
301
    }
302
303
    /**
304
     * @return string[]
305
     */
306 503
    private function normalizePath(string $attribute): array
307
    {
308 503
        $attribute = str_replace(['][', '['], '.', rtrim($attribute, ']'));
309 503
        return StringHelper::parsePath($attribute);
310
    }
311
312
    /**
313
     * @psalm-param array<array-key, array{0:int|string, 1:mixed}> $keys
314
     */
315 6
    private function makePropertyPathString(array $keys): string
316
    {
317 6
        $path = '';
318 6
        foreach ($keys as $key) {
319 6
            if ($path !== '') {
320 6
                if (is_object($key[1])) {
321 6
                    $path .= '::' . $key[0];
322
                } elseif (is_array($key[1])) {
323 6
                    $path .= '[' . $key[0] . ']';
324
                }
325
            } else {
326 6
                $path = (string) $key[0];
327
            }
328
        }
329 6
        return $path;
330
    }
331
}
332