ldc-crm-client/src/Models/ClientModel.php
Jérémy Munsch 23fdfca0be
Tests OK
2023-10-05 12:16:43 +02:00

350 lines
11 KiB
PHP

<?php
namespace LDC\LDCCRM\Models;
use LDC\LDCCRM\Enums\ArcadiaOrderType;
use LDC\LDCCRM\Enums\Country;
use LDC\LDCCRM\Enums\LogLevel;
use LDC\LDCCRM\Enums\QuestionType;
use LDC\LDCCRM\Exceptions\AccessError;
use LDC\LDCCRM\Exceptions\ValidationException;
use LDC\LDCCRM\Transport\ApiInterface;
use LDC\LDCCRM\Utils;
/**
* @ignore
* Base Class for crm client models
*
* @category Api Models
* @package LDCCrmClient
* @subpackage models
* @author Jérémy Munsch <jeremy.munsch@ldc.fr>
*/
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')));
}
}
}