290 lines
11 KiB
PHP
290 lines
11 KiB
PHP
<?php
|
||
|
||
namespace Rinsvent\Data2DTO;
|
||
|
||
use ReflectionProperty;
|
||
use Rinsvent\AttributeExtractor\ClassExtractor;
|
||
use Rinsvent\AttributeExtractor\PropertyExtractor;
|
||
use Rinsvent\Data2DTO\Attribute\DTOMeta;
|
||
use Rinsvent\Data2DTO\Attribute\PropertyPath;
|
||
use Rinsvent\Data2DTO\Attribute\VirtualProperty;
|
||
use Rinsvent\Data2DTO\Resolver\TransformerResolverStorage;
|
||
use Rinsvent\Data2DTO\Transformer\Meta;
|
||
use Rinsvent\Data2DTO\Transformer\TransformerInterface;
|
||
use function Symfony\Component\String\u;
|
||
|
||
class Data2DtoConverter
|
||
{
|
||
public function convert(array $data, object $object, array $tags = []): object
|
||
{
|
||
$tags = empty($tags) ? ['default'] : $tags;
|
||
$reflectionObject = new \ReflectionObject($object);
|
||
|
||
$this->processClassTransformers($reflectionObject, $data, $tags);
|
||
if (is_object($data)) {
|
||
return $data;
|
||
}
|
||
|
||
$properties = $reflectionObject->getProperties();
|
||
/** @var \ReflectionProperty $property */
|
||
foreach ($properties as $property) {
|
||
// todo добавить атрибут на пропуск обработки и еще атрибут на допустимые поля
|
||
|
||
/** @var \ReflectionNamedType $reflectionPropertyType */
|
||
$reflectionPropertyType = $property->getType();
|
||
$propertyType = $reflectionPropertyType->getName();
|
||
|
||
if ($this->processVirtualProperty($object, $property, $data, $tags)) {
|
||
continue;
|
||
}
|
||
|
||
$value = $this->grabValue($property, $data, $tags);
|
||
|
||
$this->processTransformers($property, $value, $tags);
|
||
|
||
if ($this->processDataObject($object, $property, $value)) {
|
||
continue;
|
||
}
|
||
|
||
if ($this->processArray($property, $value, $tags)) {
|
||
continue;
|
||
}
|
||
|
||
$preparedPropertyType = $propertyType;
|
||
if ($this->processInterface($property, $preparedPropertyType)) {
|
||
continue;
|
||
}
|
||
|
||
if ($this->processClass($object, $property, $preparedPropertyType, $value, $tags)) {
|
||
continue;
|
||
}
|
||
|
||
if ($this->processNull($reflectionPropertyType, $value)) {
|
||
continue;
|
||
}
|
||
|
||
$this->setValue($object, $property, $value);
|
||
}
|
||
|
||
return $object;
|
||
}
|
||
|
||
/**
|
||
* Для виртуальных полей добавляем пустой масиив, чтобы заполнить поля дто
|
||
*/
|
||
protected function processVirtualProperty(object $object, \ReflectionProperty $property, array $data, array $tags): bool
|
||
{
|
||
$propertyExtractor = new PropertyExtractor($property->class, $property->getName());
|
||
if ($propertyExtractor->fetch(VirtualProperty::class)) {
|
||
if ($property->isInitialized($object) && $propertyValue = $property->getValue($object)) {
|
||
$value = $this->convert($data, $propertyValue, $tags);
|
||
} else {
|
||
$propertyType = $property->getType()->getName();
|
||
$value = $this->convert($data, new $propertyType, $tags);
|
||
}
|
||
// присваиваем получившееся значение
|
||
$this->setValue($object, $property, $value);
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
* В данных лежит объект, то дальше его не заполняем. Только присваиваем. Например, entity, document
|
||
*/
|
||
protected function processDataObject(object $object, \ReflectionProperty $property, $value): bool
|
||
{
|
||
if (is_object($value)) {
|
||
$this->setValue($object, $property, $value);
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
protected function processInterface(ReflectionProperty $property, &$preparedPropertyType): bool
|
||
{
|
||
if (interface_exists($preparedPropertyType)) {
|
||
$attributedPropertyClass = $this->grabPropertyDTOClass($property);
|
||
// Если не указали мета информацию для интерфейса - пропустим
|
||
if (!$attributedPropertyClass) {
|
||
return true;
|
||
}
|
||
// Если класс не реализует интерфейс свойства - пропустим
|
||
$interfaces = class_implements($attributedPropertyClass);
|
||
if (!isset($interfaces[$preparedPropertyType])) {
|
||
return true;
|
||
}
|
||
$preparedPropertyType = $attributedPropertyClass;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
* Если это class, то рекурсивно заполняем дальше
|
||
*/
|
||
protected function processClass(object $object, ReflectionProperty $property, string $preparedPropertyType, &$value, array $tags): bool
|
||
{
|
||
if (class_exists($preparedPropertyType)) {
|
||
$propertyValue = $property->getValue($object);
|
||
if (!is_array($propertyValue)) {
|
||
return true;
|
||
}
|
||
if ($property->isInitialized($object)) {
|
||
$value = $this->convert($value, $propertyValue, $tags);
|
||
} else {
|
||
$value = $this->convert($value, new $preparedPropertyType, $tags);
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
protected function grabValue(\ReflectionProperty $property, array $data, array $tags)
|
||
{
|
||
if ($dataPath = $this->grabDataPath($property, $data, $tags)) {
|
||
return $data[$dataPath] ?? null;
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
protected function grabDataPath(\ReflectionProperty $property, array $data, array $tags): ?string
|
||
{
|
||
$propertyName = $property->getName();
|
||
$propertyExtractor = new PropertyExtractor($property->class, $propertyName);
|
||
/** @var PropertyPath $propertyPath */
|
||
if ($propertyPath = $propertyExtractor->fetch(PropertyPath::class)) {
|
||
$filteredTags = array_diff($tags, $propertyPath->tags);
|
||
if (count($filteredTags) !== count($tags)) {
|
||
if (array_key_exists($propertyPath->path, $data)) {
|
||
return $propertyPath->path;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (array_key_exists($propertyName, $data)) {
|
||
return $propertyName;
|
||
}
|
||
|
||
$variants = [
|
||
u($propertyName)->camel()->toString(),
|
||
u($propertyName)->snake()->toString(),
|
||
u($propertyName)->snake()->upper()->toString(),
|
||
];
|
||
foreach ($variants as $variant) {
|
||
if (array_key_exists($variant, $data)) {
|
||
return $variant;
|
||
}
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* Трнансформируем на уровне класса
|
||
*/
|
||
protected function processClassTransformers(\ReflectionObject $object, &$data, array $tags): void
|
||
{
|
||
$className = $object->getName();
|
||
$classExtractor = new ClassExtractor($className);
|
||
/** @var Meta $transformMeta */
|
||
while ($transformMeta = $classExtractor->fetch(Meta::class)) {
|
||
$transformMeta->returnType = $className;
|
||
$filteredTags = array_diff($tags, $transformMeta->tags);
|
||
if (count($filteredTags) === count($tags)) {
|
||
continue;
|
||
}
|
||
|
||
$transformer = $this->grabTransformer($transformMeta);
|
||
$transformer->transform($data, $transformMeta);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Трнансформируем на уровне свойст объекта
|
||
*/
|
||
protected function processTransformers(\ReflectionProperty $property, &$data, array $tags): void
|
||
{
|
||
$propertyName = $property->getName();
|
||
$propertyExtractor = new PropertyExtractor($property->class, $propertyName);
|
||
/** @var Meta $transformMeta */
|
||
while ($transformMeta = $propertyExtractor->fetch(Meta::class)) {
|
||
$filteredTags = array_diff($tags, $transformMeta->tags);
|
||
if (count($filteredTags) === count($tags)) {
|
||
continue;
|
||
}
|
||
/** @var \ReflectionNamedType $reflectionPropertyType */
|
||
$reflectionPropertyType = $property->getType();
|
||
$propertyType = $reflectionPropertyType->getName();
|
||
$transformMeta->returnType = $propertyType;
|
||
$transformMeta->allowsNull = $reflectionPropertyType->allowsNull();
|
||
$transformer = $this->grabTransformer($transformMeta);
|
||
$transformer->transform($data, $transformMeta);
|
||
}
|
||
}
|
||
|
||
protected function grabTransformer(Meta $meta): TransformerInterface
|
||
{
|
||
$storage = TransformerResolverStorage::getInstance();
|
||
$resolver = $storage->get($meta::TYPE);
|
||
return $resolver->resolve($meta);
|
||
}
|
||
|
||
/**
|
||
* Если значение в $data = null, но поле не может его принять - пропустим
|
||
*/
|
||
private function processNull(\ReflectionNamedType $reflectionPropertyType, $value): bool
|
||
{
|
||
return $value === null && !$reflectionPropertyType->allowsNull();
|
||
}
|
||
|
||
private function processArray(\ReflectionProperty $property, &$value, array $tags): bool
|
||
{
|
||
$attributedPropertyClass = $this->grabPropertyDTOClass($property);
|
||
|
||
/** @var \ReflectionNamedType $reflectionPropertyType */
|
||
$reflectionPropertyType = $property->getType();
|
||
$propertyType = $reflectionPropertyType->getName();
|
||
|
||
// Если массив и есть атрибут с указанием класса, то также преобразуем структуру
|
||
if ($propertyType === 'array' && $attributedPropertyClass) {
|
||
// Если тип у ДТО - массив, а в значении не массив - пропустим
|
||
if (!is_array($value)) {
|
||
return true;
|
||
}
|
||
$tempValue = [];
|
||
foreach ($value as $itemValue) {
|
||
if (!is_array($itemValue)) {
|
||
continue;
|
||
}
|
||
$tempValue[] = $this->convert($itemValue, new $attributedPropertyClass, $tags);
|
||
}
|
||
$value = $tempValue;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
private function grabPropertyDTOClass(\ReflectionProperty $property): ?string
|
||
{
|
||
$propertyName = $property->getName();
|
||
$propertyExtractor = new PropertyExtractor($property->class, $propertyName);
|
||
/** @var DTOMeta $dtoMeta */
|
||
if ($dtoMeta = $propertyExtractor->fetch(DTOMeta::class)) {
|
||
return $dtoMeta->class;
|
||
}
|
||
return null;
|
||
}
|
||
|
||
private function setValue(object $object, \ReflectionProperty $property, $value)
|
||
{
|
||
if (!$property->isPublic()) {
|
||
$property->setAccessible(true);
|
||
}
|
||
|
||
$property->setValue($object, $value);
|
||
|
||
if (!$property->isPublic()) {
|
||
$property->setAccessible(false);
|
||
}
|
||
}
|
||
}
|