ldc-crm-client/src/Models/CrmClientModel.php

339 lines
9.7 KiB
PHP
Raw Normal View History

<?php
namespace LDC\LDCCRM\Models;
use Exception;
2021-08-16 13:22:34 +02:00
use JsonSerializable;
use Kwaadpepper\Enum\BaseEnum;
use LDC\LDCCRM\Enums\ArcadiaOrderType;
use LDC\LDCCRM\Enums\Country;
use LDC\LDCCRM\Enums\QuestionType;
use LDC\LDCCRM\Exceptions\ValidationException;
use LDC\LDCCRM\Transport\ApiInterface;
use LDC\LDCCRM\Utils;
use ReflectionClass;
use ReflectionProperty;
use TypeError;
/**
* @ignore
* Base Class for crm client models
*
* @category Api Models
* @package LDCCrmClient
* @subpackage models
*/
2021-08-16 13:22:34 +02:00
abstract class CrmClientModel implements JsonSerializable
{
/**
* Is true if this model was not filled from crm
*
* @var boolean
*/
private $isNew = true;
/**
* The Api object that will handle requests and responses
*
* @var \LDC\LDCCRM\Transport\ApiInterface
*/
protected static $apiInterface;
/**
* Validation rules
*
* @var array
*/
protected static $validation = [];
/**
* Model fields
*
* @var array
*/
protected $modelFields = [];
/**
* The crm fields
* that should not be visible
*
* @var array
*/
protected $invisibleFields = [];
/**
* Model updated values
*/
protected $updatedValues = [];
public function __construct()
{
self::setUpApiInterface();
return $this;
}
public function __get(string $field)
{
$props = \array_diff(
$this->modelFields,
$this->invisibleFields
);
if (in_array($field, $props)) {
return self::getRawProperty($this, $field);
}
return null;
}
/**
* Set prop
*
* @param string $field
* @param mixed $value
* @throws TypeError if the field has an incorrect value
* @return mixed|null The input value or null is this prop does not exists
*/
public function __set(string $field, $value)
{
if (in_array($field, $this->modelFields)) {
$type = $this->getPropType($field);
if (!$this->checkFieldType($type, $value)) {
throw new TypeError("Wrong type for $field");
}
$this->validateValue($field, $value);
$this->updatedValues[$field] = $value;
return $this->setRawProperty($this, $field, $value);
}
return null;
}
protected static function setUpApiInterface()
{
self::$apiInterface = self::$apiInterface ?: new ApiInterface();
}
/**
* Fill values into model
*
* @param array $properties
* @return void
2021-10-14 11:46:11 +02:00
* @throws TypeError if a field has an incorrect value
*/
public function fill(array $properties)
{
if (count($properties)) {
foreach ($properties as $field => $value) {
$type = $this->getPropType($field);
2021-10-26 10:26:20 +02:00
// Force boolean
if ($type === 'bool') {
$value = (\is_int($value) and ($value === 1 or $value === 0)) ? (bool)$value : $value;
2021-10-26 10:26:20 +02:00
}
if (!$this->checkFieldType($type, $value)) {
throw new TypeError("Wrong type for $field");
}
2021-10-14 11:46:11 +02:00
$this->validateValue($field, $value);
$this->updatedValues[$field] = $value;
self::setRawProperty($this, $field, $value);
}
}
}
/**
* Set all properties to without any validation
*
* @param array $properties
* @return void
*/
protected function fillRaw(array $properties)
{
if (count($properties)) {
foreach ($properties as $field => $value) {
self::setRawProperty($this, $field, $value);
}
}
}
/**
* Validate all model properties
*
* @return array the validated values
*/
protected function validateAllValues(array $ignore = []): array
{
$out = [];
foreach (static::$validation as $field => $validationRules) {
if (\in_array($field, $ignore) or \is_null(self::getRawProperty($this, $field))) {
continue;
}
$this->validateValue($field, self::getRawProperty($this, $field));
$out[$field] = self::getRawProperty($this, $field);
}
return $out;
}
/**
* Validate a value against its rules
*
* @param string $field
* @param mixed $value
* @return boolean
* @throws ValidationException
*/
protected static function validateValue(string $field, $value): bool
{
if (!isset(static::$validation[$field])) {
throw new Exception("$field has no validation rule");
}
$validator = Utils::makeValidator(
[$field => $value],
[$field => static::$validation[$field]]
);
if ($validator()) {
throw new ValidationException(implode(' ', $validator->errors()->get($field)));
}
return true;
}
/**
* Check a value against its field type
*
* @param string $type
* @param mixed $value
* @return boolean
* @throws Exception if type is not handled handled types are bool, string,
* int, \LDC\LDCCRM\Enums\Country, \LDC\LDCCRM\Enums\ArcadiaOrderType
*/
protected function checkFieldType(string $type, $value): bool
{
// all fields nullable, TODO check this
if (is_null($value)) {
return true;
}
switch ($type) {
case '\\' . Country::class:
try {
$value = \is_int($value) ? Country::make($value) : $value;
} finally {
return \is_object($value) and (get_class($value) === Country::class or
\in_array(Country::class, \class_parents($value)));
}
case '\\' . QuestionType::class:
try {
$value = \is_int($value) ? QuestionType::make($value) : $value;
} finally {
return \is_object($value) and (get_class($value) === QuestionType::class or
\in_array(QuestionType::class, \class_parents($value)));
}
case '\\' . ArcadiaOrderType::class:
try {
$value = \is_int($value) ? ArcadiaOrderType::make($value) : $value;
} finally {
return \is_object($value) and (get_class($value) === ArcadiaOrderType::class or
\in_array(ArcadiaOrderType::class, \class_parents($value)));
}
case 'bool':
$value = \is_int($value) ? (bool)$value : $value;
return \is_bool($value);
case 'array':
return \is_array($value);
case 'string':
return \is_string($value);
case 'int':
return \is_int($value);
default:
throw new Exception(\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
*/
protected function getPropType(string $property)
{
2021-12-06 00:16:13 +01:00
$matches = [];
$propReflection = new ReflectionProperty(static::class, $property);
2021-12-06 00:16:13 +01:00
$docComment = $propReflection->getDocComment();
preg_match_all('/@var ([\w_\x5c]+)/', $docComment, $matches);
if (isset($matches[1][0]) and in_array($matches[1][0], [
'string', 'int', 'bool', 'array', '\\' . Country::class, '\\' . ArcadiaOrderType::class,
'\\' . QuestionType::class
], true)) {
return $matches[1][0];
}
return null;
}
/**
* Set a property withou any validation
*
* @param string $field
* @param mixed $value
* @return void
*/
protected static function setRawProperty(object $object, string $field, $value): void
{
if (is_object($value) and in_array(BaseEnum::class, class_parents($value))) {
$value = $value->value;
}
$refClass = new ReflectionClass(\get_class($object));
2021-12-06 00:16:13 +01:00
$refProp = $refClass->getProperty($field);
$refProp->setAccessible(true);
$refProp->setValue($object, $value);
}
/**
* Get a property
*
* @param string $field
* @param mixed $value
* @return mixed
*/
protected static function getRawProperty(object $object, string $field)
{
$refClass = new ReflectionClass(\get_class($object));
2021-12-06 00:16:13 +01:00
$refProp = $refClass->getProperty($field);
$refProp->setAccessible(true);
return $refProp->getValue($object);
}
2021-08-11 17:26:30 +02:00
/**
* Assert primaryKey is valid
*
* @param int $primaryKey
* @return void
* @throws LDC\LDCCRM\Exceptions\ValidationException
*/
protected static function assertValidPrimaryKey(int $primaryKey): void
{
$validator = Utils::makeValidator(
['id' => $primaryKey],
['id' => 'required|int']
);
if ($validator()) {
2021-08-11 17:26:30 +02:00
throw new ValidationException(implode(' ', $validator->errors()->get('id')));
}
}
/**
* Assert hashcode is valid
*
* @param string|null $hashcode
* @return void
* @throws LDC\LDCCRM\Exceptions\ValidationException
*/
protected static function assertValidHascode(?string $hashcode): void
{
$validator = Utils::makeValidator(
['hashcode' => $hashcode],
['hashcode' => 'required|uuid']
);
if ($validator()) {
2021-08-11 17:26:30 +02:00
throw new ValidationException(implode(' ', $validator->errors()->get('hashcode')));
}
}
}