babaev-an ebfd42a88e 20250222
[О] [ClassMapper::GetDefaults]: Улучшено определение типа. Теперь проверка integer не вызовет ошибку, что ожидается int.

[О] [ClassMapper::MapClass]: Теперь идёт проверка свойства на доступность get и set. Свойства с только get и только set пропускаются.
2025-02-22 13:09:54 +03:00

399 lines
17 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace goodboyalex\php_components_pack\classes;
use DateTimeImmutable;
use DateTimeInterface;
use Error;
use Exception;
use ReflectionClass;
use ReflectionException;
use stdClass;
use Throwable;
use UnitEnum;
/**
* Класс сопоставлений классов и объектов.
*
* @author Александр Бабаев
* @package php_components_pack
* @version 1.0
* @since 1.0
*/
final class ClassMapper
{
/**
* @var array $DefaultOptions Настройки
*/
public const array DefaultOptions = [
'ignored' => [],
'allowed' => []
];
/**
* Передаёт одинаковые параметры из класса $from в класс $to, учитывая игнорируемые ($ignoredProperties) и
* разрешенные ($allowedProperties) свойства.
*
* @param object $from Класс-донор
* @param object $to Класс-приемник
* @param array $options Параметры привязки свойств (см атрибут Bind).
*
* @return void
*/
public static function MapClass (object $from, object $to, array $options = self::DefaultOptions): void
{
// Если есть игнорируемые или разрешенные свойства
if (!(count($options['ignored']) == 0 && count($options['allowed']) == 0))
// -- то для каждого игнорируемого свойства
foreach ($options['ignored'] as $ignoredProperty)
// --- и если оно есть в массиве разрешенных
if (in_array($ignoredProperty, $options['allowed']))
// ---- то исключаю его из массива разрешенных
unset($options['allowed'][array_search($ignoredProperty, $options['allowed'])]);
// Задаю массив свойств
$properties = [];
// Получаю имя исходного класса
$className = get_class($from);
// Получение всех свойств класса
try {
$reflection = new ReflectionClass($className);
}
catch (ReflectionException) {
return;
}
// Получение всех свойств класса
$props = $reflection->getProperties();
// Создаю экземпляр класса
$instance = new $className();
// Для каждого свойства
foreach ($props as $prop) {
// - получаю имя свойства
$propName = $prop->getName();
// - получаю значение свойства
$value = $from->$propName;
try {
// - получаю тип свойства
$typeOf = gettype($from->$propName);
// - получаю значение свойства по типу и по умолчанию
$writeValue = self::GetDefaults($typeOf);
try {
// - проверяем, можно ли записать и прочитать значение
// -- пытаюсь установить значение
$instance->$propName = $writeValue;
// -- пытаюсь прочитать установленное значение
$readValue = $instance->$propName;
// -- и проверяю, что значение совпадают
/** @noinspection PhpConditionAlreadyCheckedInspection */
if ($readValue !== $writeValue)
continue;
}
catch (Throwable) {
// - в случае ошибки, понимаем, что свойство доступно или только для чтения, или
// только для записи и оно нам не подходит. Поэтому пропускаем его.
continue;
}
// - если свойство игнорируется
if (in_array($propName, $options['ignored']))
// -- пропускаю
continue;
// - если свойство не разрешено
if (count($options['allowed']) > 0 && !in_array($propName, $options['allowed']))
// -- пропускаю
continue;
// Если не было ошибки, значит свойство имеет и геттер, и сеттер
$properties[$propName] = $value;
}
catch (Error) {
// - в случае ошибки, понимаем, что свойство нам не подходит. Поэтому пропускаю его.
continue;
}
}
// Для каждого элемента массива
foreach ($properties as $name => $value) {
// - если свойство есть в объекте
if (property_exists($to, $name))
// -- то присваиваю значение
$to->$name = $from->$name;
}
}
/**
* Возвращает значение по умолчанию для типа $typeName.
*
* @param string $typeName Тип
*
* @return mixed Значение по умолчанию
*/
public static function GetDefaults (string $typeName): mixed
{
return match (strtolower($typeName)) {
'int', 'integer' => 0,
'float', 'double' => 0.0,
'bool', 'boolean' => false,
'string' => '',
'array' => [],
'object' => new stdClass(),
default => null,
};
}
/**
* Подготавливает значения свойств класса.
*
* @param array $params Данные запроса.
* @param ReflectionClass $classReflector Анализатор класса.
* @param array $options Массив свойств привязки.
*
* @return array Массив данных класса.
*/
public
static function PrepareClassProperties (array $params, ReflectionClass $classReflector,
array $options = self::DefaultOptions): array
{
// Создаю массив данных класса
$classData = [];
// Для каждого свойства класса
foreach ($classReflector->getProperties() as $property) {
// - получаю имя свойства
$propertyName = $property->getName();
// - если это свойство задано в массиве параметров
if (array_key_exists($propertyName, $params)) {
// -- если задан массив разрешённых свойств
if (!empty($options["allowed"]))
// --- если свойство не разрешено
if (!in_array($propertyName, $options["allowed"]))
// ---- то пропускаю
continue;
// -- если задан массив запрещённых свойств
if (!empty($options["ignored"]))
// --- если свойство должно игнорироваться
if (in_array($propertyName, $options["ignored"]))
// ---- то пропускаю
continue;
// -- добавляю значение свойства в результат
$classData[$propertyName] = $params[$propertyName];
}
else {
// - в противном случае, пробегаю массив параметров
foreach ($params as $key => $value) {
// -- если в имени параметра есть разделитель "_"
if (str_starts_with($key, $propertyName . "_")) {
// -- разбиваю имя параметра на части
$keyParts = explode("_", $key);
// -- добавляю значение свойства в результат
self::GetClassParametersArrayParser($classData, $keyParts, $value);
}
}
}
}
// Возвращаю массив данных класса
return $classData;
}
/**
* Парсит массив свойств класса.
*
* @param array $source Исходный массив (он же и результат парсинга).
* @param array $parametersKeys Массив имен свойств. Например, Page_Meta_Id должен быть разбит на
* ["Page", "Meta", "Id"].
* @param mixed $value Значение свойства.
* @param array $options Массив параметров привязки свойств.
*
* @return void
*/
public
static function GetClassParametersArrayParser (array &$source, array $parametersKeys, mixed $value,
array $options = self::DefaultOptions): void
{
// Если массив имен свойств пустой
if (empty($parametersKeys))
// - то прерываю парсинг
return;
// Получаю имя текущего свойства
$currentName = array_shift($parametersKeys);
// Если текущего свойства нет в массиве
if (!isset($source[$currentName]))
// - то создаю его
$source[$currentName] = [];
// Если массив имен свойств содержит только одно свойство
if (count($parametersKeys) === 0) {
// - если задан массив разрешённых свойств
if (!empty($options["allowed"]))
// --- если свойство не разрешено
if (!in_array($currentName, $options["allowed"]))
// ---- то пропускаю
return;
// -- если задан массив запрещённых свойств
if (!empty($options["ignored"]))
// --- если свойство должно игнорироваться
if (in_array($currentName, $options["ignored"]))
// ---- то пропускаю
return;
// - добавляю значение свойства в результат
$source[$currentName] = $value;
}
else
// - иначе продолжаю парсинг
self::GetClassParametersArrayParser($source[$currentName], $parametersKeys, $value, $options);
}
/**
* Переводит данные из массива в объект класса.
*
* @param string $className Имя класса
* @param array $properties Массив данных
*
* @return mixed Объект класса
* @throws Exception
*/
public
static function MapToClassProperty (string $className, array $properties): mixed
{
// Создаю
try {
$classReflector = new ReflectionClass($className);
// Создаю объект класса
$classObj = new $className();
// Для каждого свойства класса
foreach ($properties as $key => $value) {
// - проверяю наличие свойства
if (!$classReflector->hasProperty($key))
// -- иду к следующему
continue;
// - получаю данные про свойство
// -- само свойство
$property = $classReflector->getProperty($key);
// -- тип свойства
$propertyType = $property->getType();
// - если значение является классом
if (!$propertyType->isBuiltin() && is_array($value)) {
// -- присваиваю объект
self::SetParameterToClass($classReflector, $key, $classObj,
self::MapToClassProperty($propertyType->getName(), $value));
// -- следующий элемент
continue;
}
// - если значение является датой
if ($classObj->$key instanceof DateTimeInterface) {
// -- получаю дату
$dateValue = DateTimeImmutable::createFromFormat('Y-m-d H:i:s', $value . " 00:00:00");
// -- если не получилось
if ($dateValue === false)
// --- то добавляю дату по умолчанию (1970-01-01 00:00:00)
$dateValue = DateTimeImmutable::createFromFormat('Y-m-d H:i:s', "1970-01-01 00:00:00");
// -- присваиваю дату
self::SetParameterToClass($classReflector, $key, $classObj, $dateValue);
// -- следующий элемент
continue;
}
// - если значение является перечислением
if ($classObj->$key instanceof UnitEnum) {
// -- если значение уже является перечислением
if ($value instanceof UnitEnum)
// --- присваиваю перечисление
self::SetParameterToClass($classReflector, $key, $classObj, $value);
else
// -- иначе
self::SetParameterToClass($classReflector, $key, $classObj,
is_numeric($value) ? $classObj->$key::FromInt($value) : $classObj->$key::FromName($value));
// -- следующий элемент
continue;
}
// - если значение является NULL
if ($value == "null") {
// -- присваиваю NULL
self::SetParameterToClass($classReflector, $key, $classObj,
is_array($key) ? [] : null);
// -- следующий элемент
continue;
}
// - присваиваю значение
self::SetParameterToClass($classReflector, $key, $classObj, $value);
}
// Возвращаю объект класса
return $classObj;
}
catch (Exception $exception) {
throw new Exception($exception->getMessage());
}
}
/**
* Присваивает значение параметра объекту класса.
*
* @param ReflectionClass $classReflector Рефлектор класса.
* @param string $propertyName Имя свойства.
* @param mixed $classObj Объект класса.
* @param mixed $value Значение.
*
* @throws Exception
*/
public
static function SetParameterToClass (ReflectionClass $classReflector, string $propertyName,
mixed $classObj, mixed $value): void
{
try {
// Получаю свойство
$property = $classReflector->getProperty($propertyName);
/**
* Устанавливаю доступ значения свойства.
*
* @noinspection PhpExpressionResultUnusedInspection
*/
$property->setAccessible(true);
// Если значение null
if ($value == null || $value == "null")
// - то присваиваю значение по умолчанию
$value = self::GetDefaults($property->getType()->getName());
// Присваиваю значение
$property->setValue($classObj, $value);
}
catch (ReflectionException $exception) {
// Выбрасываю исключение
throw new Exception($exception->getMessage());
}
}
}