Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
78.22% covered (warning)
78.22%
79 / 101
41.67% covered (danger)
41.67%
5 / 12
CRAP
0.00% covered (danger)
0.00%
0 / 1
ClientModel
78.22% covered (warning)
78.22%
79 / 101
41.67% covered (danger)
41.67%
5 / 12
73.81
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 __get
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 __set
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
5
 setUpApiInterface
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 fill
47.06% covered (danger)
47.06%
8 / 17
0.00% covered (danger)
0.00%
0 / 1
24.84
 fillRaw
30.00% covered (danger)
30.00%
3 / 10
0.00% covered (danger)
0.00%
0 / 1
9.49
 validateAllValues
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
4
 validateValue
87.50% covered (warning)
87.50%
7 / 8
0.00% covered (danger)
0.00%
0 / 1
2.01
 checkFieldType
93.33% covered (success)
93.33%
14 / 15
0.00% covered (danger)
0.00%
0 / 1
12.04
 getPropType
83.33% covered (warning)
83.33%
10 / 12
0.00% covered (danger)
0.00%
0 / 1
4.07
 assertValidPrimaryKey
83.33% covered (warning)
83.33%
5 / 6
0.00% covered (danger)
0.00%
0 / 1
2.02
 assertValidHascode
83.33% covered (warning)
83.33%
5 / 6
0.00% covered (danger)
0.00%
0 / 1
2.02
1<?php
2
3namespace LDC\LDCCRM\Models;
4
5use LDC\LDCCRM\Enums\ArcadiaOrderType;
6use LDC\LDCCRM\Enums\Country;
7use LDC\LDCCRM\Enums\LogLevel;
8use LDC\LDCCRM\Enums\QuestionType;
9use LDC\LDCCRM\Exceptions\AccessError;
10use LDC\LDCCRM\Exceptions\ValidationException;
11use LDC\LDCCRM\Transport\ApiInterface;
12use LDC\LDCCRM\Utils;
13
14/**
15 * @ignore
16 * Base Class for crm client models
17 *
18 * @category Api Models
19 * @package LDCCrmClient
20 * @subpackage models
21 * @author Jérémy Munsch <jeremy.munsch@ldc.fr>
22 */
23abstract class ClientModel implements \JsonSerializable
24{
25    /**
26     * Is true if this model was not filled from crm
27     *
28     * @var boolean
29     */
30    protected bool $isNew = true;
31
32    /**
33     * The Api object that will handle requests and responses
34     *
35     * @var \LDC\LDCCRM\Transport\ApiInterface
36     */
37    protected static ApiInterface $apiInterface;
38
39    /**
40     * Validation rules
41     *
42     * @var array
43     */
44    protected static array $validation = [];
45
46    /**
47     * Model fields
48     *
49     * @var array
50     */
51    protected array $modelFields = [];
52
53    /**
54     * The crm fields
55     * that should not be visible
56     *
57     * @var array
58     */
59    protected array $invisibleFields = [];
60
61    /**
62     * Model updated values
63     *
64     * @var array
65     */
66    protected array $updatedValues = [];
67
68    /**
69     * CrmClientModel
70     */
71    public function __construct()
72    {
73        self::setUpApiInterface();
74        return $this;
75    }
76
77    /**
78     * Generic accessor
79     *
80     * @param string $field
81     * @return mixed|null Returns null if the property was not found any way.
82     */
83    public function __get(string $field)
84    {
85        $props = \array_diff(
86            $this->modelFields,
87            $this->invisibleFields
88        );
89        if (in_array($field, $props)) {
90            return Utils::getRawProperty($this, $field);
91        }
92        return null;
93    }
94
95    /**
96     * Set prop on the model.
97     *
98     * @param string $field
99     * @param mixed  $value
100     * @return void
101     * @throws \LDC\LDCCRM\Exceptions\ValidationException If a field could not be validated.
102     * @throws \LDC\LDCCRM\Exceptions\AccessError If trying to set a value on a unknown property.
103     */
104    public function __set(string $field, #[\SensitiveParameter] mixed $value): void
105    {
106        try {
107            if (in_array($field, $this->modelFields)) {
108                $type = $this->getPropType($field);
109                if (!is_null($value) and !$this->checkFieldType($type, $value)) {
110                    throw new ValidationException("Wrong type for $field");
111                }
112                $this->validateValue($field, $value);
113                $this->updatedValues[$field] = $value;
114                Utils::setRawProperty($this, $field, $value);
115                return;
116            }
117            // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch
118        } catch (\ReflectionException $e) {
119            // Do nothing.
120        }
121        throw new AccessError(\get_class($this), $field);
122    }
123
124    /**
125     * Create the Api interface
126     *
127     * @return void
128     */
129    protected static function setUpApiInterface()
130    {
131        self::$apiInterface = self::$apiInterface ?? new ApiInterface();
132    }
133
134    /**
135     * Fill values into model using validation rules.
136     *
137     * @param array $properties
138     * @return void
139     * @throws \LDC\LDCCRM\Exceptions\ValidationException If a field could not be validated.
140     */
141    public function fill(#[\SensitiveParameter] array $properties)
142    {
143        if (count($properties)) {
144            foreach ($properties as $field => $value) {
145                try {
146                    $type = $this->getPropType($field);
147                    // * Force boolean
148                    if ($type === 'bool') {
149                        $value = (\is_int($value) and ($value === 1 or $value === 0)) ? (bool)$value : $value;
150                    }
151                    if (!is_null($value) and !$this->checkFieldType($type, $value)) {
152                        throw new ValidationException("Wrong type for $field");
153                    }
154                    $this->validateValue($field, $value);
155                    $this->updatedValues[$field] = $value;
156                    Utils::setRawProperty($this, $field, $value);
157                } catch (AccessError | \ReflectionException) {
158                    Utils::log(\sprintf(
159                        '%s::fill : unknown property access for %s, it does not exist on object.',
160                        \get_class($this),
161                        $field
162                    ), LogLevel::DEBUG);
163                    continue;
164                }
165            } //end foreach
166        } //end if
167    }
168
169    /**
170     * Set all properties to without any validation
171     *
172     * @param array $properties
173     * @return void
174     */
175    protected function fillRaw(#[\SensitiveParameter] array $properties)
176    {
177        if (count($properties)) {
178            foreach ($properties as $field => $value) {
179                try {
180                    Utils::setRawProperty($this, $field, $value);
181                } catch (AccessError $e) {
182                    Utils::log(\sprintf(
183                        '%s::fillRaw : unknown property access for %s, it does not exist on object.',
184                        \get_class($this),
185                        $field
186                    ), LogLevel::DEBUG);
187                    continue;
188                }
189            }
190        }
191    }
192
193    /**
194     * Validate all model properties
195     *
196     * * All properties are nullable
197     *
198     * @param array $ignore
199     * @return array the validated values
200     * @throws \LDC\LDCCRM\Exceptions\ValidationException If a field could not be validated.
201     */
202    protected function validateAllValues(array $ignore = []): array
203    {
204        $out = [];
205        foreach (static::$validation as $field => $validationRules) {
206            if (\in_array($field, $ignore) or \is_null(Utils::getRawProperty($this, $field))) {
207                continue;
208            }
209            $this->validateValue($field, Utils::getRawProperty($this, $field));
210            $out[$field] = Utils::getRawProperty($this, $field);
211        }
212        return $out;
213    }
214
215    /**
216     * Validate a value against its rules
217     *
218     * @param string $field
219     * @param mixed  $value
220     * @return boolean
221     * @throws \ErrorException If a field has no validation rule.
222     * @throws \LDC\LDCCRM\Exceptions\ValidationException If a field could not be validated.
223     * @phpcs:ignore Squiz.Commenting.FunctionCommentThrowTag.WrongNumber
224     */
225    protected static function validateValue(string $field, #[\SensitiveParameter] mixed $value): bool
226    {
227        if (!isset(static::$validation[$field])) {
228            throw new \ErrorException("$field has no validation rule");
229        }
230        $validator = Utils::makeValidator(
231            [$field => $value],
232            [$field => static::$validation[$field]]
233        );
234        /** @throws \LDC\LDCCRM\Exceptions\ValidationException */
235        $validator();
236        return true;
237    }
238
239    /**
240     * Check a value against its field type
241     *
242     * @param string $type
243     * @param mixed  $value
244     * @return boolean Return true if $type matches $value type.
245     * @throws \ErrorException If type is not handled,
246     * handled types are boolean, string, integer, \LDC\LDCCRM\Enums\Country,  \LDC\LDCCRM\Enums\ArcadiaOrderType .
247     * @phpcs:ignore Generic.Files.LineLength.TooLong
248     */
249    protected function checkFieldType(string $type, #[\SensitiveParameter] mixed $value): bool
250    {
251        /**
252         * Checks if the value is valid for a specific enum class
253         *
254         * @param string $className
255         * @param mixed  $value
256         * @return boolean
257         */
258        $checkValueIsEnum = function (string $className, $value) {
259            try {
260                $value = \is_int($value) ? $className::make($value) : $value;
261            } finally {
262                return \is_object($value) and (get_class($value) === $className or
263                    \in_array($className, \class_parents($value)));
264            }
265        };
266
267        switch ($type) {
268            case '\\' . Country::class:
269            case '\\' . QuestionType::class:
270            case '\\' . ArcadiaOrderType::class:
271                return $checkValueIsEnum(\ltrim($type, '\\'), $value);
272            case 'boolean':
273                return \is_bool(\is_int($value) ? (bool)$value : $value);
274            case 'array':
275                return \is_array($value);
276            case 'string':
277                return \is_string($value);
278            case 'integer':
279                return \is_int($value);
280        }
281        throw new \ErrorException(\sprintf("Unhandled $type for %s", \json_encode($value)));
282    }
283
284    /**
285     * Get the type of any of this class property
286     * from doc comment
287     *
288     * @param string $property
289     * @return string|null the type or null if not defined
290     * @throws \ErrorException If the property has no comment to read.
291     * @throws \ErrorException If a type for a property could ne be recognized.
292     * @throws \ReflectionException If the class or property does not exist.
293     * @phpcs:ignore Squiz.Commenting.FunctionCommentThrowTag.WrongNumber
294     */
295    protected function getPropType(string $property)
296    {
297        $matches        = [];
298        $propReflection = new \ReflectionProperty(static::class, $property);
299        $docComment     = $propReflection->getDocComment();
300        if (!$docComment) {
301            throw new \ErrorException(\sprintf("%s $property has not comment to parse.", \get_class($this)));
302        }
303        preg_match_all('/@var ([\w_\x5c]+)/', $docComment, $matches);
304        if (
305            isset($matches[1][0]) and \in_array($matches[1][0], [
306                'string', 'integer', 'boolean', 'array', '\\' . Country::class, '\\' . ArcadiaOrderType::class,
307                '\\' . QuestionType::class
308            ], true)
309        ) {
310            return $matches[1][0];
311        }
312        throw new \ErrorException("Unknown type for property $property");
313    }
314
315    /**
316     * Assert primaryKey is valid.
317     *
318     * @param mixed $primaryKey
319     * @return void
320     * @throws \LDC\LDCCRM\Exceptions\ValidationException If a primary key is not a valid integer.
321     */
322    protected static function assertValidPrimaryKey(mixed $primaryKey): void
323    {
324        $validator = Utils::makeValidator(
325            ['id' => $primaryKey],
326            ['id' => 'required|int']
327        );
328        if ($validator()) {
329            throw new ValidationException(implode(' ', $validator->errors()->get('id')));
330        }
331    }
332
333    /**
334     * Assert hashcode is valid.
335     *
336     * @param mixed $hashcode
337     * @return void
338     * @throws \LDC\LDCCRM\Exceptions\ValidationException If a hashcode is not a valid UUID.
339     */
340    protected static function assertValidHascode(mixed $hashcode): void
341    {
342        $validator = Utils::makeValidator(
343            ['hashcode' => $hashcode],
344            ['hashcode' => 'required|uuid']
345        );
346        if ($validator()) {
347            throw new ValidationException(implode(' ', $validator->errors()->get('hashcode')));
348        }
349    }
350}