Code Coverage
 
Classes and Traits
Functions and Methods
Lines
Total
0.00% covered (danger)
0.00%
0 / 1
41.67% covered (danger)
41.67%
5 / 12
CRAP
78.79% covered (warning)
78.79%
78 / 99
CrmClientModel
0.00% covered (danger)
0.00%
0 / 1
41.67% covered (danger)
41.67%
5 / 12
73.86
78.79% covered (warning)
78.79%
78 / 99
 __construct
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
2 / 2
 __get
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
6 / 6
 __set
100.00% covered (success)
100.00%
1 / 1
5
100.00% covered (success)
100.00%
10 / 10
 setUpApiInterface
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
2 / 2
 fill
0.00% covered (danger)
0.00%
0 / 1
20.42
52.94% covered (warning)
52.94%
9 / 17
 fillRaw
0.00% covered (danger)
0.00%
0 / 1
7.46
40.00% covered (danger)
40.00%
4 / 10
 validateAllValues
100.00% covered (success)
100.00%
1 / 1
4
100.00% covered (success)
100.00%
7 / 7
 validateValue
0.00% covered (danger)
0.00%
0 / 1
2.01
85.71% covered (warning)
85.71%
6 / 7
 checkFieldType
0.00% covered (danger)
0.00%
0 / 1
12.23
88.24% covered (warning)
88.24%
15 / 17
 getPropType
0.00% covered (danger)
0.00%
0 / 1
4.18
77.78% covered (warning)
77.78%
7 / 9
 assertValidPrimaryKey
0.00% covered (danger)
0.00%
0 / 1
2.02
83.33% covered (warning)
83.33%
5 / 6
 assertValidHascode
0.00% covered (danger)
0.00%
0 / 1
2.02
83.33% covered (warning)
83.33%
5 / 6
1<?php
2
3namespace LDC\LDCCRM\Models;
4
5use JsonSerializable;
6use LDC\LDCCRM\Enums\ArcadiaOrderType;
7use LDC\LDCCRM\Enums\Country;
8use LDC\LDCCRM\Enums\LogLevel;
9use LDC\LDCCRM\Enums\QuestionType;
10use LDC\LDCCRM\Exceptions\AccessError;
11use LDC\LDCCRM\Exceptions\ValidationException;
12use LDC\LDCCRM\Transport\ApiInterface;
13use LDC\LDCCRM\Utils;
14
15/**
16 * @ignore
17 * Base Class for crm client models
18 *
19 * @category Api Models
20 * @package LDCCrmClient
21 * @subpackage models
22 * @author Jérémy Munsch <jeremy.munsch@ldc.fr>
23 */
24abstract class CrmClientModel implements JsonSerializable
25{
26    /**
27     * Is true if this model was not filled from crm
28     *
29     * @var boolean
30     */
31    protected $isNew = true;
32
33    /**
34     * The Api object that will handle requests and responses
35     *
36     * @var \LDC\LDCCRM\Transport\ApiInterface
37     */
38    protected static $apiInterface;
39
40    /**
41     * Validation rules
42     *
43     * @var array
44     */
45    protected static $validation = [];
46
47    /**
48     * Model fields
49     *
50     * @var array
51     */
52    protected $modelFields = [];
53
54    /**
55     * The crm fields
56     * that should not be visible
57     *
58     * @var array
59     */
60    protected $invisibleFields = [];
61
62    /**
63     * Model updated values
64     *
65     * @var array
66     */
67    protected $updatedValues = [];
68
69    /**
70     * CrmClientModel
71     */
72    public function __construct()
73    {
74        self::setUpApiInterface();
75        return $this;
76    }
77
78    /**
79     * Generic accessor
80     *
81     * @param string $field
82     * @return mixed|null Returns null if the property was not found any way.
83     */
84    public function __get(string $field)
85    {
86        $props = \array_diff(
87            $this->modelFields,
88            $this->invisibleFields
89        );
90        if (in_array($field, $props)) {
91            return Utils::getRawProperty($this, $field);
92        }
93        return null;
94    }
95
96    /**
97     * Set prop on the model.
98     *
99     * @param string $field
100     * @param mixed  $value
101     * @return void
102     * @throws \LDC\LDCCRM\Exceptions\ValidationException If a field could not be validated.
103     * @throws \LDC\LDCCRM\Exceptions\AccessError If trying to set a value on a unknown property.
104     */
105    public function __set(string $field, $value): void
106    {
107        try {
108            if (in_array($field, $this->modelFields)) {
109                $type = $this->getPropType($field);
110                if (!is_null($value) and !$this->checkFieldType($type, $value)) {
111                    throw new ValidationException("Wrong type for $field");
112                }
113                $this->validateValue($field, $value);
114                $this->updatedValues[$field] = $value;
115                Utils::setRawProperty($this, $field, $value);
116                return;
117            }
118            // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch
119        } catch (\ReflectionException $e) {
120            // Do nothing.
121        }
122        throw new AccessError(\get_class($this), $field);
123    }
124
125    /**
126     * Create the Api interface
127     *
128     * @return void
129     */
130    protected static function setUpApiInterface()
131    {
132        self::$apiInterface = self::$apiInterface ?: new ApiInterface();
133    }
134
135    /**
136     * Fill values into model using validation rules.
137     *
138     * @param array $properties
139     * @return void
140     * @throws \LDC\LDCCRM\Exceptions\ValidationException If a field could not be validated.
141     */
142    public function fill(array $properties)
143    {
144        if (count($properties)) {
145            foreach ($properties as $field => $value) {
146                try {
147                    $type = $this->getPropType($field);
148                    // * Force boolean
149                    if ($type === 'bool') {
150                        $value = (\is_int($value) and ($value === 1 or $value === 0)) ? (bool)$value : $value;
151                    }
152                    if (!is_null($value) and !$this->checkFieldType($type, $value)) {
153                        throw new ValidationException("Wrong type for $field");
154                    }
155                    $this->validateValue($field, $value);
156                    $this->updatedValues[$field] = $value;
157                    Utils::setRawProperty($this, $field, $value);
158                } catch (AccessError | \ReflectionException $e) {
159                    Utils::log(\sprintf(
160                        '%s::fill : unknown property access for %s, it does not exist on object.',
161                        \get_class($this),
162                        $field
163                    ), LogLevel::DEBUG());
164                    continue;
165                }
166            } //end foreach
167        }//end if
168    }
169
170    /**
171     * Set all properties to without any validation
172     *
173     * @param array $properties
174     * @return void
175     */
176    protected function fillRaw(array $properties)
177    {
178        if (count($properties)) {
179            foreach ($properties as $field => $value) {
180                try {
181                    Utils::setRawProperty($this, $field, $value);
182                } catch (AccessError $e) {
183                    Utils::log(\sprintf(
184                        '%s::fillRaw : unknown property access for %s, it does not exist on object.',
185                        \get_class($this),
186                        $field
187                    ), LogLevel::DEBUG());
188                    continue;
189                }
190            }
191        }
192    }
193
194    /**
195     * Validate all model properties
196     *
197     * * All properties are nullable
198     *
199     * @param array $ignore
200     * @return array the validated values
201     * @throws \LDC\LDCCRM\Exceptions\ValidationException If a field could not be validated.
202     */
203    protected function validateAllValues(array $ignore = []): array
204    {
205        $out = [];
206        foreach (static::$validation as $field => $validationRules) {
207            if (\in_array($field, $ignore) or \is_null(Utils::getRawProperty($this, $field))) {
208                continue;
209            }
210            $this->validateValue($field, Utils::getRawProperty($this, $field));
211            $out[$field] = Utils::getRawProperty($this, $field);
212        }
213        return $out;
214    }
215
216    /**
217     * Validate a value against its rules
218     *
219     * @param string $field
220     * @param mixed  $value
221     * @return boolean
222     * @throws \ErrorException If a field has no validation rule.
223     * @throws \LDC\LDCCRM\Exceptions\ValidationException If a field could not be validated.
224     * @phpcs:ignore Squiz.Commenting.FunctionCommentThrowTag.WrongNumber
225     */
226    protected static function validateValue(string $field, $value): bool
227    {
228        if (!isset(static::$validation[$field])) {
229            throw new \ErrorException("$field has no validation rule");
230        }
231        $validator = Utils::makeValidator(
232            [$field => $value],
233            [$field => static::$validation[$field]]
234        );
235        /** @throws \LDC\LDCCRM\Exceptions\ValidationException */
236        $validator();
237        return true;
238    }
239
240    /**
241     * Check a value against its field type
242     *
243     * @param string $type
244     * @param mixed  $value
245     * @return boolean Return true if $type matches $value type.
246     * @throws \ErrorException If type is not handled,
247     * handled types are boolean, string, integer, \LDC\LDCCRM\Enums\Country,  \LDC\LDCCRM\Enums\ArcadiaOrderType .
248     * @phpcs:ignore Generic.Files.LineLength.TooLong
249     */
250    protected function checkFieldType(string $type, $value): bool
251    {
252        /**
253         * Checks if the value is valid for a specific enum class
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($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($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}