Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
97.30% covered (success)
97.30%
72 / 74
33.33% covered (danger)
33.33%
1 / 3
CRAP
0.00% covered (danger)
0.00%
0 / 1
Mapper
97.30% covered (success)
97.30%
72 / 74
33.33% covered (danger)
33.33%
1 / 3
32
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 mapping
98.61% covered (success)
98.61%
71 / 72
0.00% covered (danger)
0.00%
0 / 1
30
 export
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3declare(strict_types=1);
4
5namespace PeServer\Core\Serialization;
6
7use PeServer\Core\Collection\Arr;
8use PeServer\Core\ReflectionUtility;
9use PeServer\Core\Serialization\Converter\TypeConverterBase;
10use PeServer\Core\Serialization\IMapper;
11use PeServer\Core\Text;
12use PeServer\Core\Throws\KeyNotFoundException;
13use PeServer\Core\Throws\MapperKeyNotFoundException;
14use PeServer\Core\Throws\MapperTypeException;
15use PeServer\Core\Throws\NotImplementedException;
16use PeServer\Core\TypeUtility;
17use ReflectionClass;
18use ReflectionNamedType;
19use TypeError;
20
21/**
22 * マッピング通常処理。
23 */
24class Mapper implements IMapper
25{
26    #region variable
27
28    /**
29     * 人畜無害なマッピング設定。
30     */
31    private readonly Mapping $defaultMapping;
32
33    #endregion
34
35    public function __construct()
36    {
37        $this->defaultMapping = new Mapping(Text::EMPTY, Mapping::FLAG_NONE);
38    }
39
40    #region IMapper
41
42    /**
43     * @SuppressWarnings(PHPMD.CyclomaticComplexity)
44     */
45    public function mapping(array $source, object $destination): void
46    {
47        $destReflection = new ReflectionClass($destination);
48        $properties = ReflectionUtility::getAllProperties($destReflection);
49
50        foreach ($properties as $property) {
51            $property->setAccessible(true);
52            $attrs = $property->getAttributes(Mapping::class);
53
54            $mapping = $this->defaultMapping;
55
56            if (!empty($attrs)) {
57                $attr = $attrs[0];
58
59                /** @var Mapping */
60                $mapping = $attr->newInstance();
61
62                // 無視する
63                if ($mapping->flags === Mapping::FLAG_IGNORE) {
64                    continue;
65                }
66            }
67
68            $keyName = $property->name;
69            if (!Text::isNullOrWhiteSpace($mapping->name)) {
70                $keyName = $mapping->name;
71            }
72
73            // 指定したキーが見つからない
74            if (!isset($source[$keyName])) {
75                if (($mapping->flags & Mapping::FLAG_EXCEPTION_NOT_FOUND_KEY) === Mapping::FLAG_EXCEPTION_NOT_FOUND_KEY) {
76                    throw new MapperKeyNotFoundException('$keyName: ' . $keyName);
77                }
78                // 例外指定がないので現在キーは無視する
79                continue;
80            }
81
82            $sourceValue = $source[$keyName];
83            unset($source[$keyName]); //対象キーは不要なので破棄
84            $sourceType = TypeUtility::getType($sourceValue);
85            $propertyTypes = ReflectionUtility::getTypes($property->getType());
86            foreach ($propertyTypes as $propertyType) {
87                $nestTypeName = $propertyType->getName();
88                $isArrayObject = $nestTypeName === TypeUtility::TYPE_ARRAY && class_exists($mapping->arrayValueClassName);
89
90                if ($sourceType === $nestTypeName && !$isArrayObject) {
91                    $property->setValue($destination, $sourceValue);
92                    continue 2; // loop: $properties
93                }
94
95                if ($sourceType === TypeUtility::TYPE_ARRAY) {
96                    $nestDestination = null;
97                    if ($property->isInitialized($destination)) {
98                        $nestDestination = $property->getValue($destination);
99                    }
100
101                    if ($nestDestination === null) {
102                        if (($mapping->flags & Mapping::FLAG_OBJECT_INSTANCE_ONLY) === Mapping::FLAG_OBJECT_INSTANCE_ONLY) {
103                            continue;
104                        }
105
106                        $nestDestination = null;
107
108                        if ($isArrayObject) {
109                            $nestDestination = [];
110                        } elseif (class_exists($nestTypeName)) {
111                            $nestDestination = new $nestTypeName();
112                        }
113
114                        $property->setValue($destination, $nestDestination);
115                    }
116
117                    if ($isArrayObject) {
118                        $isListArray = ($mapping->flags & Mapping::FLAG_LIST_ARRAY_VALUES) === Mapping::FLAG_LIST_ARRAY_VALUES;
119                        foreach ($sourceValue as $key => $value) {
120                            $objClassName = $mapping->arrayValueClassName;
121                            $obj = new $objClassName();
122                            $this->mapping($value, $obj);
123                            if ($isListArray) {
124                                $nestDestination[] = $obj;
125                            } else {
126                                $nestDestination[$key] = $obj;
127                            }
128                        }
129                        $property->setValue($destination, $nestDestination);
130                    } else {
131                        $this->mapping($sourceValue, $nestDestination);
132                    }
133
134                    continue 2; // loop: $properties
135                }
136
137                if (!Text::isNullOrWhiteSpace($mapping->converter)) {
138                    /** @phpstan-var TypeConverterBase<mixed> */
139                    $converter = new $mapping->converter();
140                    $convertedValue = $converter->read($keyName, $propertyType, $sourceValue);
141                    $property->setValue($destination, $convertedValue);
142                    continue 2; // loop: $properties
143                }
144            }
145
146            // 型一致せずにここまで来たので可能な限り元の型に合わせる
147            if (($mapping->flags & Mapping::FLAG_EXCEPTION_TYPE_MISMATCH) === Mapping::FLAG_EXCEPTION_TYPE_MISMATCH) {
148                // ただし型変換失敗例外指定の場合は全部諦める
149                throw new MapperTypeException($keyName . '(' . $sourceType . '): ' . Text::join('|', Arr::map($propertyTypes, fn (ReflectionNamedType $p) => $p->getName())));
150            }
151
152            foreach ($propertyTypes as $propertyType) {
153                // 常識的な型だけに限定(独断と偏見)
154                switch ($propertyType->getName()) {
155                    case TypeUtility::TYPE_INTEGER:
156                    case TypeUtility::TYPE_STRING:
157                    case TypeUtility::TYPE_BOOLEAN:
158                    case TypeUtility::TYPE_DOUBLE:
159                    case TypeUtility::TYPE_NULL:
160                        if (settype($sourceValue, $propertyType->getName())) {
161                            $property->setValue($destination, $sourceValue);
162                            continue 2; // loop: $properties
163                        }
164                        break;
165
166                    default:
167                        break;
168                }
169            }
170        }
171    }
172
173    public function export(object $source): array
174    {
175        throw new NotImplementedException('いるか?');
176    }
177
178    #endregion
179}