*/ abstract class ClientModel implements \JsonSerializable { /** * Is true if this model was not filled from crm * * @var boolean */ protected bool $isNew = true; /** * The Api object that will handle requests and responses * * @var \LDC\LDCCRM\Transport\ApiInterface */ protected static ApiInterface $apiInterface; /** * Validation rules * * @var array */ protected static array $validation = []; /** * Model fields * * @var array */ protected array $modelFields = []; /** * The crm fields * that should not be visible * * @var array */ protected array $invisibleFields = []; /** * Model updated values * * @var array */ protected array $updatedValues = []; /** * CrmClientModel */ public function __construct() { self::setUpApiInterface(); return $this; } /** * Generic accessor * * @param string $field * @return mixed|null Returns null if the property was not found any way. */ public function __get(string $field) { $props = \array_diff( $this->modelFields, $this->invisibleFields ); if (in_array($field, $props)) { return Utils::getRawProperty($this, $field); } return null; } /** * Set prop on the model. * * @param string $field * @param mixed $value * @return void * @throws \LDC\LDCCRM\Exceptions\ValidationException If a field could not be validated. * @throws \LDC\LDCCRM\Exceptions\AccessError If trying to set a value on a unknown property. */ public function __set(string $field, #[\SensitiveParameter] mixed $value): void { try { if (in_array($field, $this->modelFields)) { $type = $this->getPropType($field); if (!is_null($value) and !$this->checkFieldType($type, $value)) { throw new ValidationException("Wrong type for $field"); } $this->validateValue($field, $value); $this->updatedValues[$field] = $value; Utils::setRawProperty($this, $field, $value); return; } // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch } catch (\ReflectionException $e) { // Do nothing. } throw new AccessError(\get_class($this), $field); } /** * Create the Api interface * * @return void */ protected static function setUpApiInterface() { self::$apiInterface = self::$apiInterface ?? new ApiInterface(); } /** * Fill values into model using validation rules. * * @param array $properties * @return void * @throws \LDC\LDCCRM\Exceptions\ValidationException If a field could not be validated. */ public function fill(#[\SensitiveParameter] array $properties) { if (count($properties)) { foreach ($properties as $field => $value) { try { $type = $this->getPropType($field); // * Force boolean if ($type === 'bool') { $value = (\is_int($value) and ($value === 1 or $value === 0)) ? (bool)$value : $value; } if (!is_null($value) and !$this->checkFieldType($type, $value)) { throw new ValidationException("Wrong type for $field"); } $this->validateValue($field, $value); $this->updatedValues[$field] = $value; Utils::setRawProperty($this, $field, $value); } catch (AccessError | \ReflectionException) { Utils::log(\sprintf( '%s::fill : unknown property access for %s, it does not exist on object.', \get_class($this), $field ), LogLevel::DEBUG); continue; } } //end foreach } //end if } /** * Set all properties to without any validation * * @param array $properties * @return void */ protected function fillRaw(#[\SensitiveParameter] array $properties) { if (count($properties)) { foreach ($properties as $field => $value) { try { Utils::setRawProperty($this, $field, $value); } catch (AccessError $e) { Utils::log(\sprintf( '%s::fillRaw : unknown property access for %s, it does not exist on object.', \get_class($this), $field ), LogLevel::DEBUG); continue; } } } } /** * Validate all model properties * * * All properties are nullable * * @param array $ignore * @return array the validated values * @throws \LDC\LDCCRM\Exceptions\ValidationException If a field could not be validated. */ protected function validateAllValues(array $ignore = []): array { $out = []; foreach (static::$validation as $field => $validationRules) { if (\in_array($field, $ignore) or \is_null(Utils::getRawProperty($this, $field))) { continue; } $this->validateValue($field, Utils::getRawProperty($this, $field)); $out[$field] = Utils::getRawProperty($this, $field); } return $out; } /** * Validate a value against its rules * * @param string $field * @param mixed $value * @return boolean * @throws \ErrorException If a field has no validation rule. * @throws \LDC\LDCCRM\Exceptions\ValidationException If a field could not be validated. * @phpcs:ignore Squiz.Commenting.FunctionCommentThrowTag.WrongNumber */ protected static function validateValue(string $field, #[\SensitiveParameter] mixed $value): bool { if (!isset(static::$validation[$field])) { throw new \ErrorException("$field has no validation rule"); } $validator = Utils::makeValidator( [$field => $value], [$field => static::$validation[$field]] ); /** @throws \LDC\LDCCRM\Exceptions\ValidationException */ $validator(); return true; } /** * Check a value against its field type * * @param string $type * @param mixed $value * @return boolean Return true if $type matches $value type. * @throws \ErrorException If type is not handled, * handled types are boolean, string, integer, \LDC\LDCCRM\Enums\Country, \LDC\LDCCRM\Enums\ArcadiaOrderType . * @phpcs:ignore Generic.Files.LineLength.TooLong */ protected function checkFieldType(string $type, #[\SensitiveParameter] mixed $value): bool { /** * Checks if the value is valid for a specific enum class * * @param string $className * @param mixed $value * @return boolean */ $checkValueIsEnum = function (string $className, $value) { try { $value = \is_int($value) ? $className::make($value) : $value; } finally { return \is_object($value) and (get_class($value) === $className or \in_array($className, \class_parents($value))); } }; switch ($type) { case '\\' . Country::class: case '\\' . QuestionType::class: case '\\' . ArcadiaOrderType::class: return $checkValueIsEnum(\ltrim($type, '\\'), $value); case 'boolean': return \is_bool(\is_int($value) ? (bool)$value : $value); case 'array': return \is_array($value); case 'string': return \is_string($value); case 'integer': return \is_int($value); } throw new \ErrorException(\sprintf("Unhandled $type for %s", \json_encode($value))); } /** * Get the type of any of this class property * from doc comment * * @param string $property * @return string|null the type or null if not defined * @throws \ErrorException If the property has no comment to read. * @throws \ErrorException If a type for a property could ne be recognized. * @throws \ReflectionException If the class or property does not exist. * @phpcs:ignore Squiz.Commenting.FunctionCommentThrowTag.WrongNumber */ protected function getPropType(string $property) { $matches = []; $propReflection = new \ReflectionProperty(static::class, $property); $docComment = $propReflection->getDocComment(); if (!$docComment) { throw new \ErrorException(\sprintf("%s $property has not comment to parse.", \get_class($this))); } preg_match_all('/@var ([\w_\x5c]+)/', $docComment, $matches); if ( isset($matches[1][0]) and \in_array($matches[1][0], [ 'string', 'integer', 'boolean', 'array', '\\' . Country::class, '\\' . ArcadiaOrderType::class, '\\' . QuestionType::class ], true) ) { return $matches[1][0]; } throw new \ErrorException("Unknown type for property $property"); } /** * Assert primaryKey is valid. * * @param mixed $primaryKey * @return void * @throws \LDC\LDCCRM\Exceptions\ValidationException If a primary key is not a valid integer. */ protected static function assertValidPrimaryKey(mixed $primaryKey): void { $validator = Utils::makeValidator( ['id' => $primaryKey], ['id' => 'required|int'] ); if ($validator()) { throw new ValidationException(implode(' ', $validator->errors()->get('id'))); } } /** * Assert hashcode is valid. * * @param mixed $hashcode * @return void * @throws \LDC\LDCCRM\Exceptions\ValidationException If a hashcode is not a valid UUID. */ protected static function assertValidHascode(mixed $hashcode): void { $validator = Utils::makeValidator( ['hashcode' => $hashcode], ['hashcode' => 'required|uuid'] ); if ($validator()) { throw new ValidationException(implode(' ', $validator->errors()->get('hashcode'))); } } }