PocketMine-MP 5.21.2 git-a6534ecbbbcf369264567d27e5ed70f7f5be9816
Loading...
Searching...
No Matches
JsonMapper.php
1<?php
24{
32 protected $logger;
33
40 public $bExceptionOnUndefinedProperty = false;
41
48 public $bExceptionOnMissingData = false;
49
59 public $bEnforceMapType = true;
60
67 public $bStrictObjectTypeChecking = false;
68
75 public $bStrictNullTypes = true;
76
82 public $bIgnoreVisibility = false;
83
90 public $bRemoveUndefinedAttributes = false;
91
98 public $classMap = array();
99
112 public $undefinedPropertyHandler = null;
113
120 protected $arInspectedClasses = array();
121
129 public $postMappingMethod = null;
130
136 public $postMappingMethodArguments = array();
137
147 public function map($json, $object)
148 {
149 if ($this->bEnforceMapType && !is_object($json)) {
150 throw new InvalidArgumentException(
151 'JsonMapper::map() requires first argument to be an object'
152 . ', ' . gettype($json) . ' given.'
153 );
154 }
155 if (!is_object($object) && (!is_string($object) || !class_exists($object))) {
156 throw new InvalidArgumentException(
157 'JsonMapper::map() requires second argument to '
158 . 'be an object or existing class name'
159 . ', ' . gettype($object) . ' given.'
160 );
161 }
162
163 if (is_string($object)) {
164 $object = $this->createInstance($object);
165 }
166
167 $strClassName = get_class($object);
168 $rc = new ReflectionClass($object);
169 $strNs = $rc->getNamespaceName();
170 $providedProperties = array();
171 foreach ($json as $key => $jvalue) {
172 $key = $this->getSafeName($key);
173 $providedProperties[$key] = true;
174
175 // Store the property inspection results so we don't have to do it
176 // again for subsequent objects of the same type
177 if (!isset($this->arInspectedClasses[$strClassName][$key])) {
178 $this->arInspectedClasses[$strClassName][$key]
179 = $this->inspectProperty($rc, $key);
180 }
181
182 list($hasProperty, $accessor, $type, $isNullable)
183 = $this->arInspectedClasses[$strClassName][$key];
184
185 if (!$hasProperty) {
186 if ($this->bExceptionOnUndefinedProperty) {
187 throw new JsonMapper_Exception(
188 'JSON property "' . $key . '" does not exist'
189 . ' in object of type ' . $strClassName
190 );
191 } else if ($this->undefinedPropertyHandler !== null) {
192 $undefinedPropertyKey = call_user_func(
193 $this->undefinedPropertyHandler,
194 $object, $key, $jvalue
195 );
196
197 if (is_string($undefinedPropertyKey)) {
198 list($hasProperty, $accessor, $type, $isNullable)
199 = $this->inspectProperty($rc, $undefinedPropertyKey);
200 }
201 } else {
202 $this->log(
203 'info',
204 'Property {property} does not exist in {class}',
205 array('property' => $key, 'class' => $strClassName)
206 );
207 }
208
209 if (!$hasProperty) {
210 continue;
211 }
212 }
213
214 if ($accessor === null) {
215 if ($this->bExceptionOnUndefinedProperty) {
216 throw new JsonMapper_Exception(
217 'JSON property "' . $key . '" has no public setter method'
218 . ' in object of type ' . $strClassName
219 );
220 }
221 $this->log(
222 'info',
223 'Property {property} has no public setter method in {class}',
224 array('property' => $key, 'class' => $strClassName)
225 );
226 continue;
227 }
228
229 if ($isNullable || !$this->bStrictNullTypes) {
230 if ($jvalue === null) {
231 $this->setProperty($object, $accessor, null);
232 continue;
233 }
234 $type = $this->removeNullable($type);
235 } else if ($jvalue === null) {
236 throw new JsonMapper_Exception(
237 'JSON property "' . $key . '" in class "'
238 . $strClassName . '" must not be NULL'
239 );
240 }
241
242 $type = $this->getFullNamespace($type, $strNs);
243 $type = $this->getMappedType($type, $jvalue);
244
245 if ($type === null || $type === 'mixed') {
246 //no given type - simply set the json data
247 $this->setProperty($object, $accessor, $jvalue);
248 continue;
249 } else if ($this->isObjectOfSameType($type, $jvalue)) {
250 $this->setProperty($object, $accessor, $jvalue);
251 continue;
252 } else if ($this->isSimpleType($type)
253 && !(is_array($jvalue) && $this->hasVariadicArrayType($accessor))
254 ) {
255 if ($this->isFlatType($type)
256 && !$this->isFlatType(gettype($jvalue))
257 ) {
258 throw new JsonMapper_Exception(
259 'JSON property "' . $key . '" in class "'
260 . $strClassName . '" is of type ' . gettype($jvalue) . ' and'
261 . ' cannot be converted to ' . $type
262 );
263 }
264 settype($jvalue, $type);
265 $this->setProperty($object, $accessor, $jvalue);
266 continue;
267 }
268
269 //FIXME: check if type exists, give detailed error message if not
270 if ($type === '') {
271 throw new JsonMapper_Exception(
272 'Empty type at property "'
273 . $strClassName . '::$' . $key . '"'
274 );
275 } else if (strpos($type, '|')) {
276 throw new JsonMapper_Exception(
277 'Cannot decide which of the union types shall be used: '
278 . $type
279 );
280 }
281
282 $array = null;
283 $subtype = null;
284 if ($this->isArrayOfType($type)) {
285 //array
286 $array = array();
287 $subtype = substr($type, 0, -2);
288 } else if (substr($type, -1) == ']') {
289 list($proptype, $subtype) = explode('[', substr($type, 0, -1));
290 if ($proptype == 'array') {
291 $array = array();
292 } else {
293 $array = $this->createInstance($proptype, false, $jvalue);
294 }
295 } else if (is_array($jvalue) && $this->hasVariadicArrayType($accessor)) {
296 $array = array();
297 $subtype = $type;
298 } else {
299 if (is_a($type, 'ArrayAccess', true)) {
300 $array = $this->createInstance($type, false, $jvalue);
301 }
302 }
303
304 if ($array !== null) {
305 if (!is_array($jvalue) && $this->isFlatType(gettype($jvalue))) {
306 throw new JsonMapper_Exception(
307 'JSON property "' . $key . '" must be an array, '
308 . gettype($jvalue) . ' given'
309 );
310 }
311
312 $cleanSubtype = $this->removeNullable($subtype);
313 $subtype = $this->getFullNamespace($cleanSubtype, $strNs);
314 $child = $this->mapArray($jvalue, $array, $subtype, $key);
315 } else if ($this->isFlatType(gettype($jvalue))) {
316 //use constructor parameter if we have a class
317 // but only a flat type (i.e. string, int)
318 if ($this->bStrictObjectTypeChecking) {
319 throw new JsonMapper_Exception(
320 'JSON property "' . $key . '" must be an object, '
321 . gettype($jvalue) . ' given'
322 );
323 }
324 $child = $this->createInstance($type, true, $jvalue);
325 } else {
326 $child = $this->createInstance($type, false, $jvalue);
327 $this->map($jvalue, $child);
328 }
329 $this->setProperty($object, $accessor, $child);
330 }
331
332 if ($this->bExceptionOnMissingData) {
333 $this->checkMissingData($providedProperties, $rc);
334 }
335
336 if ($this->bRemoveUndefinedAttributes) {
337 $this->removeUndefinedAttributes($object, $providedProperties);
338 }
339
340 if ($this->postMappingMethod !== null
341 && $rc->hasMethod($this->postMappingMethod)
342 ) {
343 $refDeserializePostMethod = $rc->getMethod(
344 $this->postMappingMethod
345 );
346 $refDeserializePostMethod->setAccessible(true);
347 $refDeserializePostMethod->invoke(
348 $object, ...$this->postMappingMethodArguments
349 );
350 }
351
352 return $object;
353 }
354
363 protected function getFullNamespace($type, $strNs)
364 {
365 if ($type === null || $type === '' || $type[0] === '\\' || $strNs === '') {
366 return $type;
367 }
368 list($first) = explode('[', $type, 2);
369 if ($this->isSimpleType($first)) {
370 return $type;
371 }
372
373 //create a full qualified namespace
374 return '\\' . $strNs . '\\' . $type;
375 }
376
387 protected function checkMissingData($providedProperties, ReflectionClass $rc)
388 {
389 foreach ($rc->getProperties() as $property) {
390 $rprop = $rc->getProperty($property->name);
391 $docblock = $rprop->getDocComment();
392 $annotations = static::parseAnnotations($docblock);
393 if (isset($annotations['required'])
394 && !isset($providedProperties[$property->name])
395 ) {
396 throw new JsonMapper_Exception(
397 'Required property "' . $property->name . '" of class '
398 . $rc->getName()
399 . ' is missing in JSON data'
400 );
401 }
402 }
403 }
404
416 protected function removeUndefinedAttributes($object, $providedProperties)
417 {
418 foreach (get_object_vars($object) as $propertyName => $dummy) {
419 if (!isset($providedProperties[$propertyName])) {
420 unset($object->{$propertyName});
421 }
422 }
423 }
424
441 public function mapArray($json, $array, $class = null, $parent_key = '')
442 {
443 $originalClass = $class;
444 foreach ($json as $key => $jvalue) {
445 $class = $this->getMappedType($originalClass, $jvalue);
446 if ($class === null) {
447 $array[$key] = $jvalue;
448 } else if ($this->isArrayOfType($class)) {
449 $array[$key] = $this->mapArray(
450 $jvalue,
451 array(),
452 substr($class, 0, -2)
453 );
454 } else if ($this->isFlatType(gettype($jvalue))) {
455 //use constructor parameter if we have a class
456 // but only a flat type (i.e. string, int)
457 if ($this->isSimpleType($class)) {
458 settype($jvalue, $class);
459 $array[$key] = $jvalue;
460 } else {
461 if ($this->bStrictObjectTypeChecking) {
462 throw new JsonMapper_Exception(
463 'JSON property'
464 . ' "' . ($parent_key ? $parent_key : '?') . '"'
465 . ' (array key "' . $key . '") must be an object, '
466 . gettype($jvalue) . ' given'
467 );
468 }
469
470 $array[$key] = $this->createInstance(
471 $class, true, $jvalue
472 );
473 }
474 } else if ($this->isFlatType($class)) {
475 throw new JsonMapper_Exception(
476 'JSON property "' . ($parent_key ? $parent_key : '?') . '"'
477 . ' is an array of type "' . $class . '"'
478 . ' but contained a value of type'
479 . ' "' . gettype($jvalue) . '"'
480 );
481 } else if (is_a($class, 'ArrayObject', true)) {
482 $array[$key] = $this->mapArray(
483 $jvalue,
484 $this->createInstance($class)
485 );
486 } else {
487 $array[$key] = $this->map(
488 $jvalue, $this->createInstance($class, false, $jvalue)
489 );
490 }
491 }
492 return $array;
493 }
494
508 protected function inspectProperty(ReflectionClass $rc, $name)
509 {
510 //try setter method first
511 $setter = 'set' . $this->getCamelCaseName($name);
512
513 if ($rc->hasMethod($setter)) {
514 $rmeth = $rc->getMethod($setter);
515 if ($rmeth->isPublic() || $this->bIgnoreVisibility) {
516 $isNullable = false;
517 $rparams = $rmeth->getParameters();
518 if (count($rparams) > 0) {
519 $isNullable = $rparams[0]->allowsNull();
520 $ptype = $rparams[0]->getType();
521 if ($ptype !== null) {
522 $typeName = $this->stringifyReflectionType($ptype);
523 //allow overriding an "array" type hint
524 // with a more specific class in the docblock
525 if ($typeName !== 'array') {
526 return array(
527 true, $rmeth,
528 $typeName,
529 $isNullable,
530 );
531 }
532 }
533 }
534
535 $docblock = $rmeth->getDocComment();
536 $annotations = static::parseAnnotations($docblock);
537
538 if (!isset($annotations['param'][0])) {
539 return array(true, $rmeth, null, $isNullable);
540 }
541 list($type) = explode(' ', trim($annotations['param'][0]));
542 return array(true, $rmeth, $type, $this->isNullable($type));
543 }
544 }
545
546 //now try to set the property directly
547 //we have to look it up in the class hierarchy
548 $class = $rc;
549 $rprop = null;
550 do {
551 if ($class->hasProperty($name)) {
552 $rprop = $class->getProperty($name);
553 }
554 } while ($rprop === null && $class = $class->getParentClass());
555
556 if ($rprop === null) {
557 //case-insensitive property matching
558 foreach ($rc->getProperties() as $p) {
559 if ((strcasecmp($p->name, $name) === 0)) {
560 $rprop = $p;
561 $class = $rc;
562 break;
563 }
564 }
565 }
566 if ($rprop !== null) {
567 if ($rprop->isPublic() || $this->bIgnoreVisibility) {
568 $docblock = $rprop->getDocComment();
569 if (PHP_VERSION_ID >= 80000 && $docblock === false
570 && $class->hasMethod('__construct')
571 ) {
572 $docblock = $class->getMethod('__construct')->getDocComment();
573 }
574 $annotations = static::parseAnnotations($docblock);
575
576 if (!isset($annotations['var'][0])) {
577 if (PHP_VERSION_ID >= 80000 && $rprop->hasType()
578 && isset($annotations['param'])
579 ) {
580 foreach ($annotations['param'] as $param) {
581 if (strpos($param, '$' . $rprop->getName()) !== false) {
582 list($type) = explode(' ', $param);
583 return array(
584 true, $rprop, $type, $this->isNullable($type)
585 );
586 }
587 }
588 }
589
590 // If there is no annotations (higher priority) inspect
591 // if there's a scalar type being defined
592 if (PHP_VERSION_ID >= 70400 && $rprop->hasType()) {
593 $rPropType = $rprop->getType();
594 $propTypeName = $this->stringifyReflectionType($rPropType);
595 if ($this->isSimpleType($propTypeName)) {
596 return array(
597 true,
598 $rprop,
599 $propTypeName,
600 $rPropType->allowsNull()
601 );
602 }
603
604 return array(
605 true,
606 $rprop,
607 '\\' . ltrim($propTypeName, '\\'),
608 $rPropType->allowsNull()
609 );
610 }
611
612 return array(true, $rprop, null, false);
613 }
614
615 //support "@var type description"
616 list($type) = explode(' ', $annotations['var'][0]);
617
618 return array(true, $rprop, $type, $this->isNullable($type));
619 } else {
620 //no setter, private property
621 return array(true, null, null, false);
622 }
623 }
624
625 //no setter, no property
626 return array(false, null, null, false);
627 }
628
636 protected function getCamelCaseName($name)
637 {
638 return str_replace(
639 ' ', '', ucwords(str_replace(array('_', '-'), ' ', $name))
640 );
641 }
642
652 protected function getSafeName($name)
653 {
654 if (strpos($name, '-') !== false) {
655 $name = $this->getCamelCaseName($name);
656 }
657
658 return $name;
659 }
660
673 protected function setProperty(
674 $object, $accessor, $value
675 ) {
676 if (!$accessor->isPublic() && $this->bIgnoreVisibility) {
677 $accessor->setAccessible(true);
678 }
679 if ($accessor instanceof ReflectionProperty) {
680 $accessor->setValue($object, $value);
681 } else if (is_array($value) && $this->hasVariadicArrayType($accessor)) {
682 $accessor->invoke($object, ...$value);
683 } else {
684 //setter method
685 $accessor->invoke($object, $value);
686 }
687 }
688
701 protected function createInstance(
702 $class, $useParameter = false, $jvalue = null
703 ) {
704 if ($useParameter) {
705 if (PHP_VERSION_ID >= 80100
706 && is_subclass_of($class, \BackedEnum::class)
707 ) {
708 return $class::from($jvalue);
709 }
710
711 return new $class($jvalue);
712 } else {
713 $reflectClass = new ReflectionClass($class);
714 $constructor = $reflectClass->getConstructor();
715 if (null === $constructor
716 || $constructor->getNumberOfRequiredParameters() > 0
717 ) {
718 return $reflectClass->newInstanceWithoutConstructor();
719 }
720 return $reflectClass->newInstance();
721 }
722 }
723
733 protected function getMappedType($type, $jvalue = null)
734 {
735 if (isset($this->classMap[$type])) {
736 $target = $this->classMap[$type];
737 } else if (is_string($type) && $type !== '' && $type[0] == '\\'
738 && isset($this->classMap[substr($type, 1)])
739 ) {
740 $target = $this->classMap[substr($type, 1)];
741 } else {
742 $target = null;
743 }
744
745 if ($target) {
746 if (is_callable($target)) {
747 $type = $target($type, $jvalue);
748 } else {
749 $type = $target;
750 }
751 }
752 return $type;
753 }
754
764 protected function isSimpleType($type)
765 {
766 return $type == 'string'
767 || $type == 'boolean' || $type == 'bool'
768 || $type == 'integer' || $type == 'int'
769 || $type == 'double' || $type == 'float'
770 || $type == 'array' || $type == 'object'
771 || $type == 'NULL'
772 || $type === 'mixed';
773 }
774
783 protected function isObjectOfSameType($type, $value)
784 {
785 if (false === is_object($value)) {
786 return false;
787 }
788
789 return is_a($value, $type);
790 }
791
802 protected function isFlatType($type)
803 {
804 return $type == 'NULL'
805 || $type == 'string'
806 || $type == 'boolean' || $type == 'bool'
807 || $type == 'integer' || $type == 'int'
808 || $type == 'double' || $type == 'float';
809 }
810
819 protected function isArrayOfType($strType)
820 {
821 return substr($strType, -2) === '[]';
822 }
823
833 protected function hasVariadicArrayType($accessor)
834 {
835 if (!$accessor instanceof ReflectionMethod) {
836 return false;
837 }
838
839 $parameters = $accessor->getParameters();
840
841 if (count($parameters) !== 1) {
842 return false;
843 }
844
845 $parameter = $parameters[0];
846
847 return $parameter->isVariadic();
848 }
849
857 protected function isNullable($type)
858 {
859 return stripos('|' . $type . '|', '|null|') !== false;
860 }
861
869 protected function removeNullable($type)
870 {
871 if ($type === null) {
872 return null;
873 }
874 return substr(
875 str_ireplace('|null|', '|', '|' . $type . '|'),
876 1, -1
877 );
878 }
879
888 protected function stringifyReflectionType(ReflectionType $type)
889 {
890 if ($type instanceof ReflectionNamedType) {
891 return ($type->isBuiltin() ? '' : '\\') . $type->getName();
892 }
893
894 return implode(
895 '|',
896 array_map(
897 function (ReflectionNamedType $type) {
898 return ($type->isBuiltin() ? '' : '\\') . $type->getName();
899 },
900 $type->getTypes()
901 )
902 );
903 }
904
914 protected static function parseAnnotations($docblock)
915 {
916 $annotations = array();
917 // Strip away the docblock header and footer
918 // to ease parsing of one line annotations
919 $docblock = substr($docblock, 3, -2);
920
921 $re = '/@(?P<name>[A-Za-z_-]+)(?:[ \t]+(?P<value>.*?))?[ \t]*\r?$/m';
922 if (preg_match_all($re, $docblock, $matches)) {
923 $numMatches = count($matches[0]);
924
925 for ($i = 0; $i < $numMatches; ++$i) {
926 $annotations[$matches['name'][$i]][] = $matches['value'][$i];
927 }
928 }
929
930 return $annotations;
931 }
932
942 protected function log($level, $message, array $context = array())
943 {
944 if ($this->logger) {
945 $this->logger->log($level, $message, $context);
946 }
947 }
948
956 public function setLogger($logger)
957 {
958 $this->logger = $logger;
959 }
960}
961?>
setLogger($logger)
removeNullable($type)
getCamelCaseName($name)
hasVariadicArrayType($accessor)
createInstance( $class, $useParameter=false, $jvalue=null)
isNullable($type)
getSafeName($name)
map($json, $object)
stringifyReflectionType(ReflectionType $type)
inspectProperty(ReflectionClass $rc, $name)
getMappedType($type, $jvalue=null)
isFlatType($type)
removeUndefinedAttributes($object, $providedProperties)
isObjectOfSameType($type, $value)
isArrayOfType($strType)
getFullNamespace($type, $strNs)
log($level, $message, array $context=array())
setProperty( $object, $accessor, $value)
static parseAnnotations($docblock)
mapArray($json, $array, $class=null, $parent_key='')
isSimpleType($type)
checkMissingData($providedProperties, ReflectionClass $rc)