<?php

/**
 * Part of JsonMapper
 *
 * PHP version 5
 *
 * @category Netresearch
 * @package  JsonMapper
 * @author   Christian Weiske <christian.weiske@netresearch.de>
 * @license  OSL-3.0 http://opensource.org/licenses/osl-3.0
 * @link     http://www.netresearch.de/
 */
namespace WPForms\Vendor\apimatic\jsonmapper;

use Exception;
use ReflectionClass;
use ReflectionException;
use ReflectionMethod;
/**
 * Automatically map JSON structures into objects.
 *
 * @category Netresearch
 * @package  JsonMapper
 * @author   Christian Weiske <christian.weiske@netresearch.de>
 * @license  OSL-3.0 http://opensource.org/licenses/osl-3.0
 * @link     http://www.netresearch.de/
 */
class JsonMapper
{
    /**
     * PSR-3 compatible logger object
     *
     * @link http://www.php-fig.org/psr/psr-3/
     * @var  object
     * @see  setLogger()
     */
    protected $logger;
    /**
     * Throw an exception when JSON data contain a property
     * that is not defined in the PHP class
     *
     * @var boolean
     */
    public $bExceptionOnUndefinedProperty = \false;
    /**
     * Calls this method on the PHP class when an undefined property
     * is found. This method should receive two arguments, $key
     * and $value for the property key and value. Only works if
     * $bExceptionOnUndefinedProperty is set to false.
     *
     * @var string
     */
    public $sAdditionalPropertiesCollectionMethod = null;
    /**
     * Throw an exception if the JSON data miss a property
     * that is marked with @required in the PHP class
     *
     * @var boolean
     */
    public $bExceptionOnMissingData = \false;
    /**
     * If the types of map() parameters shall be checked.
     * You have to disable it if you're using the json_decode "assoc" parameter.
     *
     *     `json_decode($str, false)`
     *
     * @var boolean
     */
    public $bEnforceMapType = \true;
    /**
     * Contains user provided map of class names vs their child classes.
     * This is only needed if discriminators are to be used. PHP reflection is not
     * used to get child classes because most code bases use autoloaders where
     * classes are lazily loaded.
     *
     * @var array
     */
    public $arChildClasses = array();
    /**
     * Contains user provided map of discriminators substitution along with
     * its actual value.
     * This is only needed if discriminators are to be used in type combinators,
     * and their actual values are substituted in the type combinator templates.
     *
     * @var array<string,string>
     */
    public $discriminatorSubs = array();
    /**
     * Runtime cache for inspected classes. This is particularly effective if
     * mapArray() is called with a large number of objects
     *
     * @var array property inspection result cache
     */
    protected $arInspectedClasses = array();
    /**
     * An array of directives from php defined configuration files.
     *
     * @var array|null Array of values from the configuration files.
     */
    protected $config = null;
    protected $zendOptimizerPlusExtensionLoaded = null;
    /**
     * Constructor for JsonMapper.
     *
     * @throws JsonMapperException
     */
    function __construct()
    {
        $zendOptimizerPlus = "Zend Optimizer+";
        $zendOptimizerPlusSaveCommentKey = "zend_optimizerplus.save_comments";
        $opCacheSaveCommentKey = "opcache.save_comments";
        if (!isset($this->config)) {
            $iniPath = \php_ini_loaded_file();
            $functionEnabled = !\in_array('parse_ini_file', \explode(',', \ini_get('disable_functions')));
            $accessAllowed = $this->isPathAllowed($iniPath, \ini_get('open_basedir'));
            if ($accessAllowed && $functionEnabled && \is_readable($iniPath)) {
                $this->config = \parse_ini_file($iniPath);
            }
        }
        if (!isset($this->zendOptimizerPlusExtensionLoaded)) {
            $this->zendOptimizerPlusExtensionLoaded = \extension_loaded($zendOptimizerPlus);
        }
        $zendOptimizerDiscardedComments = $this->zendOptimizerPlusExtensionLoaded === \true && $this->commentsDiscardedFor($zendOptimizerPlusSaveCommentKey);
        $opCacheDiscardedComments = $this->commentsDiscardedFor($opCacheSaveCommentKey);
        if ($zendOptimizerDiscardedComments || $opCacheDiscardedComments) {
            throw JsonMapperException::commentsDisabledInConfigurationException(array($zendOptimizerPlusSaveCommentKey, $opCacheSaveCommentKey));
        }
    }
    /**
     * Returns true if the provided file path is accessible and
     * not restricted by open_basedir restriction.
     *
     * @param string|false $filePath     Real file path to be checked.
     * @param string|false $allowedPaths Allowed paths separated by os
     *                                   path separator.
     *
     * @return bool Whether the provided path is allowed to access.
     */
    protected function isPathAllowed($filePath, $allowedPaths)
    {
        if (empty($filePath)) {
            return \false;
        }
        if (empty($allowedPaths)) {
            return \true;
        }
        $allowedPathArray = \explode(\PATH_SEPARATOR, $allowedPaths);
        if (!\in_array(\dirname($filePath), $allowedPathArray)) {
            return \false;
        }
        return \true;
    }
    /**
     * Returns true if comments are disabled locally or in php.ini file.
     * However, if comments are enabled locally by overwriting global
     * php.ini configurations then returns false.
     *
     * @param string $configKey Configuration key to be checked.
     *
     * @return bool Whether comments are disabled in environment or php.ini file.
     */
    protected function commentsDiscardedFor($configKey)
    {
        $localConfigVal = \strtolower(\ini_get($configKey));
        $phpIniConfigVal = !isset($this->config[$configKey]) ? '' : \strtolower($this->config[$configKey]);
        $enableValues = ["1", "on", "true", "yes"];
        $disableValues = ["0", "off", "false", "no"];
        $notEnabled = \in_array($localConfigVal, $enableValues, \true) === \false;
        $isDisabled = \in_array($localConfigVal, $disableValues, \true) === \true;
        $isDisabledInPhpIniFile = \in_array($phpIniConfigVal, $disableValues, \true) === \true;
        return $notEnabled && ($isDisabled || $isDisabledInPhpIniFile);
    }
    /**
     * Map data all data in $json into the given $object instance.
     *
     * @param object $json   JSON object structure from json_decode()
     * @param object $object Object to map $json data into
     * @param bool   $strict True if looking to map with strict type checking,
     *                       Default: false
     *
     * @return object Mapped object is returned.
     * @see    mapArray()
     */
    public function map($json, $object, $strict = \false)
    {
        if ($this->bEnforceMapType && !\is_object($json)) {
            throw new \InvalidArgumentException('JsonMapper::map() requires first argument to be an object' . ', ' . \gettype($json) . ' given.');
        }
        if (!\is_object($object)) {
            throw new \InvalidArgumentException('JsonMapper::map() requires second argument to be an object' . ', ' . \gettype($object) . ' given.');
        }
        $strClassName = \get_class($object);
        $rc = new ReflectionClass($object);
        $providedProperties = array();
        $additionalPropertiesMethod = $this->getAdditionalPropertiesMethod($rc);
        foreach ($json as $key => $jvalue) {
            // $providedProperties[$key] = true;
            $isAdditional = \false;
            // Store the property inspection results so we don't have to do it
            // again for subsequent objects of the same type
            if (!isset($this->arInspectedClasses[$strClassName][$key])) {
                $this->arInspectedClasses[$strClassName][$key] = $this->inspectProperty($rc, $key);
            }
            list($hasProperty, $accessor, $type, $factoryMethod, $mapsBy, $namespace) = $this->arInspectedClasses[$strClassName][$key];
            if ($accessor !== null) {
                $providedProperties[$accessor->getName()] = \true;
            }
            if (!$hasProperty) {
                if ($this->bExceptionOnUndefinedProperty) {
                    throw JsonMapperException::undefinedPropertyException($key, $strClassName);
                }
                $isAdditional = \true;
                $this->log('info', 'Property {property} does not exist in {class}', array('property' => $key, 'class' => $strClassName));
            }
            if ($accessor === null) {
                if ($this->bExceptionOnUndefinedProperty) {
                    throw JsonMapperException::undefinedPropertyException($key, $strClassName, \true);
                }
                $isAdditional = \true;
                $this->log('info', 'Property {property} has no public setter method in {class}', array('property' => $key, 'class' => $strClassName));
            }
            //FIXME: check if type exists, give detailled error message if not
            if ($type === '') {
                throw JsonMapperException::missingTypePropertyException($key, $strClassName);
            }
            if ($isAdditional) {
                $this->addAdditionalProperty($additionalPropertiesMethod, $object, $key, $jvalue);
                continue;
            }
            $value = $this->getMappedValue($jvalue, $type, $mapsBy, $factoryMethod, $namespace, $rc->getName(), $strict);
            $this->setProperty($object, $accessor, $value);
        }
        if ($this->bExceptionOnMissingData) {
            $this->checkMissingData($providedProperties, $rc);
        }
        return $object;
    }
    /**
     * Add additional properties by invoking the specified method.
     *
     * @param ReflectionMethod|null $method Method to be called to add
     *                                      additional properties to the class.
     * @param object                $object Class instance on which the method
     *                                      is invoked.
     * @param int|string            $key    The name of additional property.
     * @param mixed                 $value  The value of additional property.
     *
     * @return void
     */
    protected function addAdditionalProperty($method, $object, $key, $value)
    {
        if (\is_null($method)) {
            return;
        }
        $annotations = $this->parseAnnotations($method->getDocComment());
        try {
            $type = $this->getDocTypeForArrayOrMixed($this->getParameterType($method->getParameters()[1]), $annotations, 1);
            $mapsBy = $this->getMapByAnnotationFromParsed($annotations);
            $factoryMethods = $this->getFactoryMethods($annotations);
            $value = $this->getMappedValue($value, $type, $mapsBy, $factoryMethods, $method->getDeclaringClass()->getNamespaceName(), $method->getDeclaringClass()->getName(), \true);
            $method->invoke($object, $key, $value);
        } catch (Exception $_) {
            // Ignore the thrown error to skip this additional property
        }
    }
    /**
     * Checks if type is an array, and extracts its dimensions and inner type.
     *
     * @param string $type       Type to be checked for array.
     * @param int    $dimensions Dimensions passed in recursions, initial: 0.
     *
     * @return array
     */
    public function getArrayTypeAndDimensions($type, $dimensions = 0)
    {
        list($isMap, $isArray, $innerType) = TypeCombination::extractTypeInfo($type);
        if ($isMap || $isArray) {
            // if it's an array or map of some type
            // increment dimension and check for innerType
            return $this->getArrayTypeAndDimensions($innerType, ++$dimensions);
        }
        return array($type, $dimensions);
    }
    /**
     * Try calling the factory method if exists, otherwise throw JsonMapperException
     *
     * @param string $factoryMethod factory method in the format
     *                              'path/to/callable/function argType'
     * @param mixed  $value         value to be passed in as param into factory
     *                              method.
     * @param string $strClassName  strClassName referencing this factory method
     *
     * @return mixed|false
     * @throws JsonMapperException
     */
    protected function callFactoryMethod($factoryMethod, $value, $strClassName)
    {
        $factoryMethod = \explode(' ', $factoryMethod)[0];
        if (!\is_callable($factoryMethod)) {
            throw JsonMapperException::unCallableFactoryMethodException($factoryMethod, $strClassName);
        }
        return \call_user_func($factoryMethod, $value);
    }
    /**
     * Try calling the given function with value, return [true, updatedValue]
     * if call successful.
     *
     * @param mixed  $value         value to be passed in argument of factory method.
     * @param string $factoryMethod factory method string in the format
     *                              'path/to/callable/function argType'.
     *
     * @return array Return an array [bool $success, $value] and value will be the
     *               failure cause if not success.
     */
    protected function callFactoryWithErrorHandling($value, $factoryMethod)
    {
        $success = \true;
        if (\version_compare(\phpversion(), '7.0', '<')) {
            try {
                $value = $this->callFactoryMethod($factoryMethod, $value, '');
            } catch (Exception $e) {
                // In Php versions < 7.0 catching only exceptions but not typeErrors
                // since strict types were not available for php < 7.0
                // also we can't use throwable since its only available after php 7.0
                $success = \false;
                $value = $e->getMessage();
            }
        } else {
            try {
                $value = $this->callFactoryMethod($factoryMethod, $value, '');
            } catch (\Throwable $e) {
                // In Php versions >= 7.0 catching exceptions including typeErrors
                // using Throwable since its base interface for Exceptions & Errors
                // since types can be strict for php >= 7.0
                $success = \false;
                $value = $e->getMessage();
            }
        }
        return [$success, $value];
    }
    /**
     * Get mapped value for a property in an object.
     *
     * @param mixed         $jvalue         Raw normalized data for the property
     * @param string        $type           Type found by inspectProperty()
     * @param string|null   $mapsBy         OneOf/AnyOf types hint found by
     *                                      inspectProperty in mapsBy annotation
     * @param string[]|null $factoryMethods Callable factory methods for property
     * @param string        $namespace      Namespace of the class
     * @param string        $className      Name of the class
     * @param bool          $strict         True if looking to map with strict
     *                                      type checking.
     *
     * @return array|false|mixed|object|null
     * @throws JsonMapperException|ReflectionException
     */
    protected function getMappedValue($jvalue, $type, $mapsBy, $factoryMethods, $namespace, $className, $strict)
    {
        if ($mapsBy) {
            return $this->mapFor($jvalue, $mapsBy, $namespace, $factoryMethods, $className);
        }
        //use factory method generated value if factory provided
        if ($factoryMethods !== null && isset($factoryMethods[0])) {
            return $this->callFactoryMethod($factoryMethods[0], $jvalue, $className);
        }
        if ($this->isNullable($type)) {
            if ($jvalue === null) {
                return null;
            }
            $type = $this->removeNullable($type);
        }
        if ($type === null || $type === 'mixed' || $type === '') {
            //no given type - simply return the json data
            return $jvalue;
        }
        if ($this->isObjectOfSameType($type, $jvalue)) {
            return $jvalue;
        }
        if ($this->isSimpleType($type)) {
            if ($strict && !$this->isSimpleValue($jvalue, $type)) {
                // if mapping strictly for multipleTypes
                throw JsonMapperException::unableToSetTypeException($type, \json_encode($jvalue));
            }
            \settype($jvalue, $type);
            return $jvalue;
        }
        list($array, $innerArrayType, $dimension) = $this->getArrayInfo($type, $namespace);
        $fullTypeName = $this->getFullNamespace($type, $namespace);
        if (\is_null($array)) {
            // Handling non array types
            if ($this->isFlatType(\gettype($jvalue)) && !$strict) {
                // use constructor parameter if we have a class
                // but only a flat type (i.e. string, int)
                if ($jvalue === null) {
                    return null;
                }
                return new $fullTypeName($jvalue);
            }
            return $this->mapClass($jvalue, $fullTypeName, $strict);
        }
        // Handling array types
        if ($jvalue === null) {
            return null;
        }
        if ($this->isNullable($innerArrayType)) {
            $innerArrayType = $this->removeNullable($innerArrayType);
        }
        $fullTypeName = $this->getFullNamespace($innerArrayType, $namespace);
        if (!$this->isSimpleType($innerArrayType)) {
            $innerArrayType = $fullTypeName;
        }
        if ($this->isRegisteredType($fullTypeName)) {
            return $this->mapClassArray($jvalue, $innerArrayType, $dimension, $strict);
        }
        return $this->mapArray($jvalue, $array, $innerArrayType, $dimension, $strict);
    }
    /**
     * Returns the complete array info with array instance, its subType and
     * its dimensions.
     *
     * @param string $type      Type to be checked for array info
     * @param string $namespace Models namespace in case the type is a model
     *
     * @return array An array where 1st element is array's instance, 2nd is
     *               inner type info, and 3rd is dimensions of array.
     * @throws ReflectionException
     */
    protected function getArrayInfo($type, $namespace)
    {
        list($subtype, $dimension) = $this->getArrayTypeAndDimensions($type);
        if ($dimension > 0) {
            return array(array(), $subtype, $dimension);
        }
        if (\substr($type, -1) == ']') {
            list($propType, $subtype) = \explode('[', \substr($type, 0, -1));
            if (!$this->isSimpleType($propType)) {
                $propType = $this->getFullNamespace($propType, $namespace);
            }
            return array($this->createInstance($propType), $subtype, $dimension);
        }
        if ($type == 'ArrayObject' || \is_subclass_of($type, 'ArrayObject')) {
            return array($this->createInstance($type), null, $dimension);
        }
        return array(null, $subtype, $dimension);
    }
    /**
     * Check if an array is Associative (has string keys) or
     * its Indexed (empty or non-string keys), returns [isAssociative, isIndexed]
     *
     * @param mixed $value A value that could be isAssociative or isIndexed array
     *
     * @return array Returns Array of result i.e [isAssociative, isIndexed]
     */
    protected function isAssociativeOrIndexed($value)
    {
        if (\is_object($value)) {
            return [\true, \false];
        }
        if (!\is_array($value)) {
            return [\false, \false];
        }
        foreach ($value as $key => $v) {
            if (\is_string($key)) {
                return [\true, \false];
            }
        }
        return [\false, \true];
    }
    /**
     * Gets not nested type for the given value
     *
     * @param mixed $value Value to be checked for type
     *
     * @return string|false Return flat PHP types for the given value
     *                      and if not flat type return false.
     */
    protected function getFlatType($value)
    {
        $type = \gettype($value);
        if (!$this->isFlatType($type)) {
            return \false;
        }
        switch ($type) {
            case 'integer':
                $type = 'int';
                break;
            case 'double':
                $type = 'float';
                break;
            case 'boolean':
                $type = 'bool';
                break;
            case 'NULL':
                $type = 'null';
                break;
        }
        return $type;
    }
    /**
     * Check all given factory methods that can be called with given value.
     *
     * @param mixed    $value          Any value to be checked with factoryMethods.
     * @param mixed    $newVal         A copy of value to be updated.
     * @param string   $type           Extracted type of the value.
     * @param string[] $factoryMethods Methods in the format 'path/to/method argType'
     *                                 which will be converting $value into any
     *                                 desirable type.
     *
     * @return string Returns the type or typeGroup of value based on
     *                given factory methods.
     * @throws JsonMapperException
     */
    protected function applyFactoryMethods($value, &$newVal, $type, $factoryMethods)
    {
        $errorMsg = [];
        $types = [$type];
        // list of possible types
        foreach ($factoryMethods as $m) {
            // checking each provided factory method
            $method = \explode(' ', $m);
            // try calling factory method
            list($success, $val) = $this->callFactoryWithErrorHandling($value, $m);
            if ($success) {
                if ($type == $method[1]) {
                    // if method call is successful
                    // and given type equals to argType of factory method
                    // update the value with returned $val of factory method
                    // and return with type early
                    $newVal = $val;
                    return $type;
                }
                // if method call is successful
                // and given type is not same as argType of factory method
                // then add argType in list of possible types for $value
                \array_push($types, $method[1]);
            } elseif ($type == $method[1]) {
                // if method call is failure given type equals to argType of
                // factory method then add reason $val as an error message
                \array_push($errorMsg, "{$method[0]}: {$val}");
            }
        }
        if (!empty($errorMsg)) {
            // if any error msg is added then throw exception
            throw JsonMapperException::invalidArgumentFactoryMethodException($type, \join("\n", $errorMsg));
        }
        // converting possible types array into the string format
        // of an anyof typeGroup
        $types = \array_unique($types);
        \asort($types);
        $type = \join(',', $types);
        if (\count($types) > 1) {
            // wrap in brackets for multiple types
            $type = "({$type})";
        }
        return $type;
    }
    /**
     * Extract type from any given value.
     *
     * @param mixed    $value   Any value to be checked for type, should be
     *                          an array if checking for inner type
     * @param string[] $factory Methods in the format 'path/to/method argType'
     *                          which will be converting $value into any
     *                          desirable type, Default: []
     * @param string   $start   string to be appended at the start of the
     *                          extracted type, Default: ''
     * @param string   $end     string to be appended at the end of the
     *                          extracted type, Default: ''
     *
     * @return string Returns the type that could be mapped on the given value.
     * @throws JsonMapperException
     */
    protected function getType(&$value, $factory = [], $start = '', $end = '')
    {
        $type = $this->getFlatType($value);
        $newVal = $value;
        if (!$type && \is_array($value)) {
            if ($this->isAssociativeOrIndexed($value)[0]) {
                // if value is associative array
                $start .= 'array<string,';
                $end = '>' . $end;
            } else {
                // if value is indexed array
                if (empty($value)) {
                    return 'array';
                }
                $end = '[]' . $end;
            }
            $types = [];
            foreach ($value as $k => $v) {
                \array_push($types, $this->getType($v, $factory));
                $newVal[$k] = $v;
            }
            $types = \array_unique($types);
            \asort($types);
            $isOneOfOrAnyOf = !empty($types) && \substr($types[0], -1) === ')';
            if (\count($types) > 1 || $isOneOfOrAnyOf) {
                // wrap in brackets for multiple types or oneof/anyof type
                $start .= '(';
                $end = ')' . $end;
            }
            $type = \join(',', $types);
        } elseif (!$type && \is_object($value)) {
            $class = \get_class($value);
            // returns full path of class
            $slashPos = \strrpos($class, '\\');
            if (!$slashPos) {
                // if slash not found then replace with -1
                $slashPos = -1;
            }
            $type = \substr($class, ++$slashPos);
        }
        $type = "{$start}{$type}{$end}";
        if (!empty($factory)) {
            $type = $this->applyFactoryMethods($value, $newVal, $type, $factory);
        }
        $value = $newVal;
        return $type;
    }
    /**
     * Check the given type/types in the provided typeGroup, return true if
     * type(s) exists in the typeGroup
     *
     * @param TypeCombination|string $typeGroup TypesCombination object or string
     *                                          format for grouped types. All kind
     *                                          of groups are allowed here.
     * @param TypeCombination|string $type      Can be a normal type like string[],
     *                                          int, Car, etc. or a combination of
     *                                          types like (CarA,CarB)[], (int,Enum),
     *                                          or array<string,(CarA,CarB)>.
     * @param string                 $start     prefix used by string $type,
     *                                          Default: ""
     * @param string                 $end       postfix used by string $type,
     *                                          Default: ""
     *
     * @return bool
     */
    protected function checkForType($typeGroup, $type, $start = '', $end = '')
    {
        if (\is_string($typeGroup)) {
            // convert into TypeCombination object
            $typeGroup = TypeCombination::withFormat($typeGroup);
        }
        if (\is_string($type) && \strpos($type, '(') !== \false) {
            // for combination of types like: (A,B)[] or array<string,(A,(B,C)[])>
            // convert into TypeCombination object
            $type = TypeCombination::withFormat($type);
        }
        $checkAllInner = \false;
        // required when $type instance of TypeCombination.
        if (\is_string($type)) {
            // for checking simple types like: string, int[] or Car[]
            if ($typeGroup->getGroupName() == 'map') {
                $start .= 'array<string,';
                $end = '>' . $end;
            } elseif ($typeGroup->getGroupName() == 'array') {
                $end = '[]' . $end;
            }
            foreach ($typeGroup->getTypes() as $t) {
                if (\is_string($t)) {
                    $matched = $type === "{$start}{$t}{$end}";
                } else {
                    $matched = $this->checkForType($t, $type, $start, $end);
                }
                if ($matched) {
                    // if any type in the typeGroup matched with given type,
                    // then early return true
                    return \true;
                }
            }
            return \false;
        } elseif (\in_array($type->getGroupName(), ['array', 'map'])) {
            // To handle type if its array/map group of types
            // extract all internal groups from the given typeGroup that
            // are similar to $type
            $typeGroup = TypeCombination::with($typeGroup->extractSimilar($type));
            // update type to the innermost level of oneof/anyof
            $type = $type->extractOneOfAnyOfGroup();
            // check all inner elements of $type
            $checkAllInner = \true;
        }
        // To handle type if its oneof/anyof group of types
        foreach ($type->getTypes() as $t) {
            $contains = $this->checkForType($typeGroup, $t);
            if (!$checkAllInner && $contains) {
                // if any type is found then
                // type is matched with $typeGroup
                return \true;
            }
            if ($checkAllInner && !$contains) {
                // if any type is missing then
                // type is not matched with $typeGroup
                return \false;
            }
        }
        return $checkAllInner;
    }
    /**
     * Converts the given typeCombination into its string format.
     *
     * @param TypeCombination|string $type Combined type/Single type.
     *
     * @return string
     */
    protected function formatType($type)
    {
        return \is_string($type) ? $type : $type->getFormat();
    }
    /**
     * Checks if type of the given value is present in the type group,
     * also updates the value when necessary.
     *
     * @param string $typeGroup      String format for grouped types, i.e.
     *                               oneof(Car,Atom)
     * @param mixed  $value          Any value to be checked in type group
     * @param array  $factoryMethods Callable factory methods for the value, that
     *                               are required to serialize it into any of the
     *                               provided types in typeGroup in the format:
     *                               'path/to/method argType', Default: []
     *
     * @return mixed Returns the same value or updated one if any factory method
     *               is applied
     * @throws JsonMapperException Throws exception if a factory method is provided
     *                             but applicable on value, or also throws an
     *                             exception if type of value didn't match with type
     *                             group
     */
    public function checkTypeGroupFor($typeGroup, $value, $factoryMethods = [])
    {
        $type = self::getType($value, $factoryMethods);
        if ($this->checkForType($typeGroup, $type)) {
            return $value;
        }
        throw JsonMapperException::unableToMapException('Type', $type, $typeGroup);
    }
    /**
     * Map the data in $value by the provided $typeGroup i.e. oneOf(A,B)
     * will try to map value with only one of A or B, that matched. While
     * anyOf(A,B) will try to map it with any of A or B and sets its type to
     * the first one that matched.
     *
     * @param mixed                  $value          Raw normalized value to be
     *                                               mapped with any typeGroup
     * @param string|TypeCombination $typeGroup      TypesCombination object or
     *                                               string format for grouped types
     * @param string                 $namespace      Namespace of any customType
     *                                               class that's present in the
     *                                               provided typeGroup.
     * @param string[]|null          $factoryMethods Callable factory methods for
     *                                               the value, that are required
     *                                               to deserialize it into any of
     *                                               the provided types in typeGroup
     *                                               like ['path/to/method argType']
     * @param string|null            $className      Name of the parent class that's
     *                                               holding this property (if any)
     *
     * @return array|mixed|object
     * @throws JsonMapperException
     */
    public function mapFor($value, $typeGroup, $namespace = '', $factoryMethods = null, $className = null)
    {
        if (\is_string($typeGroup)) {
            // convert into TypeCombination object
            $typeGroup = TypeCombination::withFormat($typeGroup, isset($factoryMethods) ? $factoryMethods : []);
        }
        $isArrayGroup = $typeGroup->getGroupName() == 'array';
        $isMapGroup = $typeGroup->getGroupName() == 'map';
        if ($isArrayGroup || $isMapGroup) {
            list($isAssociative, $isIndexed) = $this->isAssociativeOrIndexed($value);
            if ($isMapGroup && !$isAssociative || $isArrayGroup && !$isIndexed) {
                // Throw exception:
                // IF value is not associative array with groupType == map
                // Or value is not indexed array with groupType == array
                $typeName = $isMapGroup ? 'Associative Array' : 'Array';
                throw JsonMapperException::unableToMapException($typeName, $this->formatType($typeGroup), \json_encode($value));
            }
            $mappedObject = [];
            foreach ($value as $k => $v) {
                $mappedObject[$k] = $this->mapFor($v, $typeGroup->getTypes()[0], $namespace, null, $className);
            }
            return $mappedObject;
        }
        return $this->checkMappingsFor($typeGroup, $value, $className, $namespace, function ($type, $value, $factoryMethods, $nspace, $className) {
            if (\is_string($type)) {
                return $this->getMappedValue($value, $type, null, $factoryMethods, $nspace, $className, \true);
            }
            return $this->mapFor($value, $type, $nspace, null, $className);
        });
    }
    /**
     * Checks mappings for all types with mappedObject, provided by
     * mappedObjectCallback.
     *
     * @param TypeCombination $typeGroup         TypesCombination object or string
     *                                           format for grouped types
     * @param mixed           $value             Mixed typed value to be checked
     *                                           by mappings with each of the types
     * @param string|null     $className         Name of the class
     * @param string          $namespace         Namespace of the class
     * @param callable        $mappedObjCallback Callback function to be called with
     *                                           each type in provided types, this
     *                                           function must return the mapped
     *                                           Object, for which the mapping will
     *                                           be checked, and to ignore any type,
     *                                           it can throw JsonMapperException
     *
     * @return false|mixed|null     Returns the final mapped object after checking
     *                              for oneOf and anyOf cases
     * @throws JsonMapperException
     */
    protected function checkMappingsFor($typeGroup, $value, $className, $namespace, $mappedObjCallback)
    {
        $mappedObject = null;
        $mappedWith = '';
        $deserializers = $typeGroup->getDeserializers();
        $selectedDeserializer = null;
        $discSubs = isset($this->discriminatorSubs) ? $this->discriminatorSubs : [];
        // check json value for each type in types array
        foreach ($typeGroup->getTypes() as $type) {
            try {
                if (\is_string($type)) {
                    list($matched, $method) = $this->isValueOfType($value, $type, $typeGroup->getDiscriminator($type, $discSubs), $namespace, $deserializers);
                    if (!$matched) {
                        // skip this type as it can't be mapped on the given value.
                        continue;
                    }
                    $selectedDeserializer = isset($method) ? [$method] : null;
                }
                $mappedObject = \call_user_func($mappedObjCallback, $type, $value, $selectedDeserializer, $namespace, $className);
            } catch (Exception $e) {
                continue;
                // ignore the type if it can't be mapped for given value
            }
            $matchedType = $type;
            if ($typeGroup->getGroupName() == 'oneOf' && $mappedWith) {
                // if its oneOf and we have a value that is already mapped,
                // then throw jsonMapperException
                throw OneOfValidationException::moreThanOneOfException($this->formatType($matchedType), $this->formatType($mappedWith), \json_encode($value));
            }
            $mappedWith = $matchedType;
            if ($typeGroup->getGroupName() == 'anyOf') {
                break;
                // break if its anyOf, and we already have mapped its value
            }
        }
        if (!$mappedWith) {
            if ($typeGroup->getGroupName() == 'oneOf') {
                throw OneOfValidationException::cannotMapAnyOfException($this->formatType($typeGroup), \json_encode($value));
            }
            throw AnyOfValidationException::cannotMapAnyOfException($this->formatType($typeGroup), \json_encode($value));
        }
        return $mappedObject;
    }
    /**
     * Checks types against the value.
     *
     * @param mixed      $value   Value to be checked
     * @param string     $type    type defined in param's typehint
     * @param array|null $disc    An array with format discriminatorFieldName
     *                            as element 1 and discriminatorValue as
     *                            element 2
     * @param string     $nspace  Namespace of the class
     * @param string[]   $methods deserializer functions array in the format
     *                            ["pathToCallableFunction typeOfValue", ...]
     *                            Default: []
     *
     * @return array array(bool $matched, ?string $method) $matched represents if
     *               Type matched with value, $method represents the selected
     *               factory method (if any)
     * @throws ReflectionException
     * @throws JsonMapperException
     */
    protected function isValueOfType($value, $type, $disc, $nspace, $methods = [])
    {
        if (!empty($methods)) {
            $methodFound = \false;
            foreach ($methods as $method) {
                if (isset($method) && \explode(' ', $method)[1] == $type) {
                    $methodFound = \true;
                    if ($this->callFactoryWithErrorHandling($value, $method)[0]) {
                        return array(\true, $method);
                    }
                }
            }
            if ($methodFound) {
                // if any method was found but couldn't deserialize value
                return array(\false, null);
            }
        }
        list($isMap, $isArray, $innerType) = TypeCombination::extractTypeInfo($type);
        if ($isMap || $isArray) {
            // if type is array like int[] or map like array<string,int>
            list($isAssociative, $isIndexed) = $this->isAssociativeOrIndexed($value);
            if ($isMap && $isAssociative || $isArray && $isIndexed) {
                // Value must be associativeArray/object for MapType
                // Or it must be indexed array for ArrayType
                foreach ($value as $v) {
                    if (!$this->isValueOfType($v, $innerType, $disc, $nspace)[0]) {
                        // false if any element is not of same type
                        return array(\false, null);
                    }
                }
                // true only if all elements in the array/map are of same type
                return array(\true, null);
            }
            return array(\false, null);
            // false if type is array/map but value is not
        }
        if ($type == 'mixed') {
            return array(\true, null);
        }
        if ($type == 'null' || $this->isSimpleType($type) || !\is_object($value)) {
            return array($this->isSimpleValue($value, $type), null);
        }
        if (!isset($disc)) {
            // if default discriminator is not provided
            // try getting it from the class annotations
            $rc = new ReflectionClass($this->getFullNamespace($type, $nspace));
            $disc = $this->getDiscriminator($rc);
        }
        return array($this->isComplexValue($value, $disc), null);
    }
    /**
     * Check if value is a complex type with provided discriminator
     *
     * @param mixed      $value         Value to be checked
     * @param array|null $discriminator An array with format discriminatorFieldName
     *                                  as element 1 and discriminatorValue as
     *                                  element 2
     *
     * @return bool True if value is a complexType with provided discriminator
     */
    protected function isComplexValue($value, $discriminator)
    {
        if (!isset($discriminator)) {
            // if discriminator is missing
            return \true;
        }
        list($discriminatorField, $discriminatorValue) = $discriminator;
        if (!isset($value->{$discriminatorField})) {
            // if value didn't have discriminatorField
            return \true;
        }
        // if discriminator field is set then decide w.r.t its value
        return $value->{$discriminatorField} == $discriminatorValue;
    }
    /**
     * Checks if the given type is a "simple type"
     *
     * @param string $type type name from gettype()
     *
     * @return boolean True if it is a simple PHP type
     */
    protected function isSimpleType($type)
    {
        return $type == 'string' || $type == 'boolean' || $type == 'bool' || $type == 'integer' || $type == 'int' || $type == 'float' || $type == 'double' || $type == 'array' || $type == 'object';
    }
    /**
     * Check if value is of simple type
     *
     * @param mixed  $value Value to be checked
     * @param string $type  Type defined in param's typehint
     *
     * @return bool True if value is of the given simple type
     */
    protected function isSimpleValue($value, $type)
    {
        return $type == 'string' && \is_string($value) || $type == 'array' && (\is_array($value) || \is_object($value)) || $type == 'object' && \is_object($value) || $type == 'bool' && \is_bool($value) || $type == 'boolean' && \is_bool($value) || $type == 'int' && \is_int($value) || $type == 'integer' && \is_int($value) || $type == 'float' && \is_float($value) || $type == 'double' && \is_float($value) || $type == 'null' && \is_null($value);
    }
    /**
     * Map all data in $json into a new instance of $type class.
     *
     * @param object|null $json   JSON object structure from json_decode()
     * @param string      $type   The type of class instance to map into.
     * @param bool        $strict True if looking to map with strict type checking,
     *                            Default: false
     *
     * @return object|null Mapped object is returned.
     * @throws ReflectionException|JsonMapperException
     * @see    mapClassArray()
     */
    public function mapClass($json, $type, $strict = \false)
    {
        if ($json === null) {
            return null;
        }
        if (!\is_object($json)) {
            throw new \InvalidArgumentException('JsonMapper::mapClass() requires first argument to be an object' . ', ' . \gettype($json) . ' given.');
        }
        $ttype = \ltrim($type, "\\");
        if (!\class_exists($type)) {
            throw new \InvalidArgumentException('JsonMapper::mapClass() requires second argument to be a class name' . ', ' . $type . ' given.');
        }
        $rc = new ReflectionClass($ttype);
        //try and find a class with matching discriminator
        $matchedRc = $this->getDiscriminatorMatch($json, $rc);
        //otherwise fallback to an instance of $type class
        if ($matchedRc === null) {
            $instance = $this->createInstance($ttype, $json, $strict);
        } else {
            $instance = $this->createInstance($matchedRc->getName(), $json, $strict);
        }
        return $this->map($json, $instance, $strict);
    }
    /**
     * Get class instance that best matches the class
     *
     * @param object|null     $json JSON object structure from json_decode()
     * @param ReflectionClass $rc   Class to get instance of. This method
     *                              will try to first match the
     *                              discriminator field with the
     *                              discriminator value of the current
     *                              class or its child class. If no
     *                              matches is found, then the current
     *                              class's instance is returned.
     *
     * @return ReflectionClass|null Object instance if match is found.
     * @throws ReflectionException
     */
    protected function getDiscriminatorMatch($json, $rc)
    {
        $discriminator = $this->getDiscriminator($rc);
        if ($discriminator) {
            list($fieldName, $fieldValue) = $discriminator;
            if (isset($json->{$fieldName}) && $json->{$fieldName} === $fieldValue) {
                return $rc;
            }
            if (!$this->isRegisteredType($rc->name)) {
                return null;
            }
            foreach ($this->getChildClasses($rc) as $clazz) {
                $childRc = $this->getDiscriminatorMatch($json, $clazz);
                if ($childRc) {
                    return $childRc;
                }
            }
        }
        return null;
    }
    /**
     * Get discriminator info
     *
     * @param ReflectionClass $rc ReflectionClass of class to inspect
     *
     * @return array|null          An array with discriminator arguments
     *                             Element 1 is discriminator field name
     *                             and element 2 is discriminator value.
     */
    protected function getDiscriminator($rc)
    {
        $annotations = $this->parseAnnotations($rc->getDocComment());
        $annotationInfo = array();
        if (isset($annotations['discriminator'])) {
            $annotationInfo[0] = \trim($annotations['discriminator'][0]);
            if (isset($annotations['discriminatorType'])) {
                $annotationInfo[1] = \trim($annotations['discriminatorType'][0]);
            } else {
                $annotationInfo[1] = $rc->getShortName();
            }
            return $annotationInfo;
        }
        return null;
    }
    /**
     * Get child classes from a ReflectionClass
     *
     * @param ReflectionClass $rc ReflectionClass of class to inspect
     *
     * @return ReflectionClass[]  ReflectionClass instances for child classes
     * @throws ReflectionException
     */
    protected function getChildClasses($rc)
    {
        $children = array();
        foreach ($this->arChildClasses[$rc->name] as $class) {
            $child = new ReflectionClass($class);
            if ($child->isSubclassOf($rc)) {
                $children[] = $child;
            }
        }
        return $children;
    }
    /**
     * Convert a type name to a fully namespaced type name.
     *
     * @param string $type  Type name (simple type or class name)
     * @param string $strNs Base namespace that gets prepended to the type name
     *
     * @return string Fully-qualified type name with namespace
     */
    protected function getFullNamespace($type, $strNs)
    {
        if (\is_string($type) && $type !== '' && $type[0] != '\\') {
            //create a full qualified namespace
            if ($strNs != '') {
                $type = '\\' . $strNs . '\\' . $type;
            }
        }
        return $type;
    }
    /**
     * Check required properties exist in json
     *
     * @param array           $providedProperties array with json properties
     * @param ReflectionClass $rc                 Reflection class to check
     *
     * @return void
     * @throws JsonMapperException
     */
    protected function checkMissingData($providedProperties, ReflectionClass $rc)
    {
        foreach ($rc->getProperties() as $property) {
            $rprop = $rc->getProperty($property->name);
            $docblock = $rprop->getDocComment();
            $annotations = $this->parseAnnotations($docblock);
            if (isset($annotations['required']) && !isset($providedProperties[$property->name])) {
                throw JsonMapperException::requiredPropertyMissingException($property->name, $rc->getName());
            }
        }
    }
    /**
     * Get additional properties setter method for the class.
     *
     * @param ReflectionClass $rc Reflection class to check
     *
     * @return ReflectionMethod    Method or null if disabled.
     */
    protected function getAdditionalPropertiesMethod(ReflectionClass $rc)
    {
        if ($this->bExceptionOnUndefinedProperty !== \false || $this->sAdditionalPropertiesCollectionMethod === null) {
            return null;
        }
        $additionalPropertiesMethod = null;
        try {
            $additionalPropertiesMethod = $rc->getMethod($this->sAdditionalPropertiesCollectionMethod);
            if (!$additionalPropertiesMethod->isPublic()) {
                throw new \InvalidArgumentException($this->sAdditionalPropertiesCollectionMethod . " method is not public on the given class.");
            }
            if ($additionalPropertiesMethod->getNumberOfParameters() < 2) {
                throw new \InvalidArgumentException($this->sAdditionalPropertiesCollectionMethod . ' method does not receive two args, $key and $value.');
            }
        } catch (\ReflectionException $_) {
            // Ignore if the method is not available on the given class
        }
        return $additionalPropertiesMethod;
    }
    /**
     * Map an array
     *
     * @param array         $jsonArray JSON array structure from json_decode()
     * @param mixed         $array     Array or ArrayObject that gets filled with
     *                                 data from $json.
     * @param string|object $class     Class name for children objects. All children
     *                                 will get mapped onto this type. Supports class
     *                                 names and simple types like "string".
     * @param int           $dimension Dimension of array to map, i.e. 2 for 2D
     *                                 array, Default: 1
     * @param bool          $strict    True if looking to map with strict type
     *                                 checking, Default: false
     *
     * @return mixed Mapped $array is returned
     */
    public function mapArray($jsonArray, $array, $class = null, $dimension = 1, $strict = \false)
    {
        foreach ($jsonArray as $key => $jvalue) {
            if ($class === null) {
                $array[$key] = $jvalue;
            } else {
                if ($dimension > 1) {
                    $array[$key] = $this->mapArray($jvalue, array(), $class, $dimension - 1, $strict);
                } else {
                    if ($this->isFlatType(\gettype($jvalue))) {
                        // use constructor parameter if we have a class
                        // but only a flat type (i.e. string, int)
                        if ($jvalue === null) {
                            $array[$key] = null;
                        } else {
                            if ($this->isSimpleType($class)) {
                                if ($strict && !$this->isSimpleValue($jvalue, $class)) {
                                    // if mapping strictly for multipleTypes
                                    throw JsonMapperException::unableToSetTypeException($class, \json_encode($jvalue));
                                }
                                \settype($jvalue, $class);
                                $array[$key] = $jvalue;
                            } else {
                                $array[$key] = new $class($jvalue);
                            }
                        }
                    } else {
                        $instance = $this->createInstance($class, $jvalue, $strict);
                        $array[$key] = $this->map($jvalue, $instance, $strict);
                    }
                }
            }
        }
        return $array;
    }
    /**
     * Map an array
     *
     * @param array|null $jsonArray JSON array structure from json_decode()
     * @param string     $type      Class name
     * @param int        $dimension Dimension of array to map, i.e. 2 for 2D array,
     *                              Default: 1
     * @param bool       $strict    True if looking to map with strict type checking,
     *                              Default: false
     *
     * @return array|null           A new array containing object of $type
     *                              which is mapped from $jsonArray
     * @throws ReflectionException|JsonMapperException
     */
    public function mapClassArray($jsonArray, $type, $dimension = 1, $strict = \false)
    {
        if ($jsonArray === null) {
            return null;
        }
        $array = array();
        foreach ($jsonArray as $key => $jvalue) {
            if ($dimension > 1) {
                $array[$key] = $this->mapClassArray($jvalue, $type, $dimension - 1, $strict);
            } else {
                $array[$key] = $this->mapClass($jvalue, $type, $strict);
            }
        }
        return $array;
    }
    /**
     * Try to find out if a property exists in a given class.
     * Checks property first, falls back to setter method.
     *
     * @param ReflectionClass $rc   Reflection class to check
     * @param string          $name Property name
     *
     * @return array First value: if the property exists
     *               Second value: the accessor to use (
     *                 ReflectionMethod or ReflectionProperty, or null)
     *               Third value: type of the property
     *               Fourth value: factory method
     */
    protected function inspectProperty(ReflectionClass $rc, $name)
    {
        $rmeth = null;
        $annotations = [];
        $mapsBy = null;
        $namespace = $rc->getNamespaceName();
        foreach ($rc->getMethods(ReflectionMethod::IS_PUBLIC) as $method) {
            $annotations = $this->parseAnnotations($method->getDocComment());
            if ($name === $this->getMapAnnotationFromParsed($annotations)) {
                $rmeth = $method;
                $mapsBy = $this->getMapByAnnotationFromParsed($annotations);
                break;
            }
        }
        if ($rmeth === null) {
            //try setter method
            $setter = 'set' . \str_replace(' ', '', \ucwords(\str_replace('_', ' ', $name)));
            if ($rc->hasMethod($setter)) {
                $rmeth = $rc->getMethod($setter);
                $annotations = $this->parseAnnotations($rmeth->getDocComment());
            }
        }
        if ($rmeth !== null && $rmeth->isPublic()) {
            $factoryMethod = $this->getFactoryMethods($annotations);
            $namespace = $rmeth->getDeclaringClass()->getNamespaceName();
            $type = null;
            $rparams = $rmeth->getParameters();
            if (\count($rparams) > 0) {
                $type = $this->getParameterType($rparams[0]);
            }
            $type = $this->getDocTypeForArrayOrMixed($type, $annotations);
            return array(\true, $rmeth, $type, $factoryMethod, $mapsBy, $namespace);
        }
        $rprop = null;
        // check for @maps annotation for hints
        foreach ($rc->getProperties(\ReflectionProperty::IS_PUBLIC) as $p) {
            $mappedName = $this->getMapAnnotation($p);
            if ($mappedName !== null && $name == $mappedName) {
                $mapsBy = $this->getMapByAnnotation($p);
                $rprop = $p;
                break;
            }
        }
        //now try to set the property directly
        if ($rprop === null) {
            if ($rc->hasProperty($name) && $this->getMapAnnotation($rc->getProperty($name)) === null) {
                $rprop = $rc->getProperty($name);
            } else {
                //case-insensitive property matching
                foreach ($rc->getProperties(\ReflectionProperty::IS_PUBLIC) as $p) {
                    if (\strcasecmp($p->name, $name) === 0 && $this->getMapAnnotation($p) === null) {
                        $rprop = $p;
                        break;
                    }
                }
            }
        }
        if ($rprop !== null) {
            if ($rprop->isPublic()) {
                $docblock = $rprop->getDocComment();
                $annotations = $this->parseAnnotations($docblock);
                $namespace = $rprop->getDeclaringClass()->getNamespaceName();
                $type = null;
                $factoryMethod = $this->getFactoryMethods($annotations);
                //support "@var type description"
                if (isset($annotations['var'][0])) {
                    list($type) = \explode(' ', $annotations['var'][0]);
                }
                return array(\true, $rprop, $type, $factoryMethod, $mapsBy, $namespace);
            } else {
                //no setter, private property
                return array(\true, null, null, null, $mapsBy, $namespace);
            }
        }
        //no setter, no property
        return array(\false, null, null, null, $mapsBy, $namespace);
    }
    /**
     * Get Phpdoc typehint for parameter
     *
     * @param \ReflectionParameter $param ReflectionParameter instance for parameter
     *
     * @return string|null
     */
    protected function getParameterType(\ReflectionParameter $param)
    {
        if (\PHP_VERSION_ID < 80000 && null !== ($class = $param->getClass())) {
            return "\\" . $class->getName();
        }
        if (\is_callable([$param, 'hasType']) && $param->hasType()) {
            $type = $param->getType();
            if ($type->isBuiltIn()) {
                $typeName = $this->reflectionTypeToString($type);
            } else {
                $typeName = "\\" . $this->reflectionTypeToString($type);
            }
            return $type->allowsNull() ? "{$typeName}|null" : $typeName;
        }
        return null;
    }
    /**
     * Get name for a ReflectionType instance
     *
     * @param \ReflectionType $type Reflection type instance
     *
     * @return string
     */
    protected function reflectionTypeToString($type)
    {
        if (\class_exists('ReflectionNamedType') && $type instanceof \ReflectionNamedType) {
            return $type->getName();
        } else {
            return (string) $type;
        }
    }
    /**
     * If the actual type is array or mixed, use the annotations to extract
     * the type.
     *
     * @param string|null $type        The actual type of the parameter.
     * @param array       $annotations The annotations to search the type.
     * @param int         $index       The position of parameter in the function.
     *
     * @return string|null
     */
    public function getDocTypeForArrayOrMixed($type, $annotations, $index = 0)
    {
        if (($type === null || $type === 'array' || $type === 'array|null') && isset($annotations['param'][$index])) {
            list($type) = \explode(' ', \trim($annotations['param'][$index]));
        }
        return $type;
    }
    /**
     * Get all factory methods from the list of annotations.
     *
     * @param array $annotations The annotations list.
     *
     * @return string[]
     */
    public function getFactoryMethods(array $annotations)
    {
        $factoryMethod = null;
        if (isset($annotations['factory'])) {
            //support "@factory method_name"
            $factoryMethod = $annotations['factory'];
        }
        return $factoryMethod;
    }
    /**
     * Get map annotation value for a property
     *
     * @param object $property Property of a class
     *
     * @return string|null     Map annotation value
     */
    protected function getMapAnnotation($property)
    {
        $annotations = $this->parseAnnotations($property->getDocComment());
        return $this->getMapAnnotationFromParsed($annotations);
    }
    /**
     * Get map annotation value from a parsed annotation list
     *
     * @param array $annotations Parsed annotation list
     *
     * @return string|null       Map annotation value
     */
    protected function getMapAnnotationFromParsed($annotations)
    {
        if (isset($annotations['maps'][0])) {
            return $annotations['maps'][0];
        }
        return null;
    }
    /**
     * Get mapBy annotation value for a property
     *
     * @param object $property Property of a class
     *
     * @return string|null     MapBy annotation value
     */
    protected function getMapByAnnotation($property)
    {
        $annotations = $this->parseAnnotations($property->getDocComment());
        return $this->getMapByAnnotationFromParsed($annotations);
    }
    /**
     * Get mapsBy annotation value from a parsed annotation list
     *
     * @param array $annotations Parsed annotation list
     *
     * @return string|null       MapsBy annotation value
     */
    protected function getMapByAnnotationFromParsed($annotations)
    {
        if (isset($annotations['mapsBy'][0])) {
            return $annotations['mapsBy'][0];
        }
        return null;
    }
    /**
     * Set a property on a given object to a given value.
     *
     * Checks if the setter or the property are public are made before
     * calling this method.
     *
     * @param object $object   Object to set property on
     * @param object $accessor ReflectionMethod or ReflectionProperty
     * @param mixed  $value    Value of property
     *
     * @return void
     */
    protected function setProperty($object, $accessor, $value)
    {
        if ($accessor instanceof \ReflectionProperty) {
            $object->{$accessor->getName()} = $value;
        } else {
            $object->{$accessor->getName()}($value);
        }
    }
    /**
     * Create a new object of the given type.
     *
     * @param string $class   Class name to instantiate
     * @param object $jobject Use jobject for constructor args
     * @param bool   $strict  True if looking to map with strict type checking,
     *                        Default: false
     *
     * @return object Freshly created object
     * @throws ReflectionException|JsonMapperException
     */
    protected function createInstance($class, &$jobject = null, $strict = \false)
    {
        $rc = new ReflectionClass($class);
        $ctor = $rc->getConstructor();
        if ($ctor === null || 0 === ($ctorReqParamsCount = $ctor->getNumberOfRequiredParameters())) {
            return new $class();
        } else {
            if ($jobject === null) {
                throw JsonMapperException::noArgumentsException($class, $ctor->getNumberOfRequiredParameters());
            }
        }
        $ctorRequiredParams = \array_slice($ctor->getParameters(), 0, $ctorReqParamsCount);
        $ctorRequiredParamsName = \array_map(function (\ReflectionParameter $param) {
            return $param->getName();
        }, $ctorRequiredParams);
        $ctorRequiredParams = \array_combine($ctorRequiredParamsName, $ctorRequiredParams);
        $ctorArgs = [];
        foreach ($jobject as $key => $jvalue) {
            if (\count($ctorArgs) === $ctorReqParamsCount) {
                break;
            }
            // Store the property inspection results so we don't have to do it
            // again for subsequent objects of the same type
            if (!isset($this->arInspectedClasses[$class][$key])) {
                $this->arInspectedClasses[$class][$key] = $this->inspectProperty($rc, $key);
            }
            list($hasProperty, $accessor, $type, $factoryMethod, $mapsBy, $namespace) = $this->arInspectedClasses[$class][$key];
            if (!$hasProperty) {
                // if no matching property or setter method found
                if (isset($ctorRequiredParams[$key])) {
                    $rp = $ctorRequiredParams[$key];
                    $jtype = null;
                } else {
                    continue;
                }
            } else {
                if ($accessor instanceof \ReflectionProperty) {
                    // if a property was found
                    if (isset($ctorRequiredParams[$accessor->getName()])) {
                        $rp = $ctorRequiredParams[$accessor->getName()];
                        $jtype = $type;
                    } else {
                        continue;
                    }
                } else {
                    // if a setter method was found
                    $methodName = $accessor->getName();
                    $methodName = \substr($methodName, 0, 3) === 'set' ? \lcfirst(\substr($methodName, 3)) : $methodName;
                    if (isset($ctorRequiredParams[$methodName])) {
                        $rp = $ctorRequiredParams[$methodName];
                        $jtype = $type;
                    } else {
                        continue;
                    }
                }
            }
            $ttype = $this->getParameterType($rp);
            if ($ttype !== null && $ttype !== 'array' && $ttype !== 'array|null' || $jtype === null) {
                // when $ttype is too generic, fallback to $jtype
                $jtype = $ttype;
            }
            $ctorArgs[$rp->getPosition()] = $this->getMappedValue($jvalue, $jtype, $mapsBy, $factoryMethod, $namespace, $rc->getName(), $strict);
            if (!$strict) {
                unset($jobject->{$key});
            }
            unset($ctorRequiredParamsName[$rp->getPosition()]);
        }
        if (\count($ctorArgs) < $ctorReqParamsCount) {
            throw JsonMapperException::fewerArgumentsException($class, $ctorRequiredParamsName);
        }
        \ksort($ctorArgs);
        return $rc->newInstanceArgs($ctorArgs);
    }
    /**
     * Checks if the object is of this type or has this type as one of its parents
     *
     * @param string $type  class name of type being required
     * @param mixed  $value Some PHP value to be tested
     *
     * @return boolean True if $object has type of $type
     */
    protected function isObjectOfSameType($type, $value)
    {
        if (\false === \is_object($value)) {
            return \false;
        }
        return \is_a($value, $type);
    }
    /**
     * Checks if the given type is a type that is not nested
     * (simple type except array and object)
     *
     * @param string $type type name from gettype()
     *
     * @return boolean True if it is a non-nested PHP type
     */
    protected function isFlatType($type)
    {
        return $type == 'NULL' || $type == 'string' || $type == 'boolean' || $type == 'bool' || $type == 'integer' || $type == 'int' || $type == 'double';
    }
    /**
     * Is type registered with mapper
     *
     * @param string|null $type Class name
     *
     * @return boolean True if registered with $this->arChildClasses
     */
    protected function isRegisteredType($type)
    {
        if (!isset($type)) {
            return \false;
        }
        return isset($this->arChildClasses[\ltrim($type, "\\")]);
    }
    /**
     * Checks if the given type is nullable
     *
     * @param string $type type name from the phpdoc param
     *
     * @return boolean True if it is nullable
     */
    protected function isNullable($type)
    {
        return \stripos('|' . $type . '|', '|null|') !== \false;
    }
    /**
     * Remove the 'null' section of a type
     *
     * @param string $type type name from the phpdoc param
     *
     * @return string The new type value
     */
    protected function removeNullable($type)
    {
        return \substr(\str_ireplace('|null|', '|', '|' . $type . '|'), 1, -1);
    }
    /**
     * Copied from PHPUnit 3.7.29, Util/Test.php
     *
     * @param string $docblock Full method docblock
     *
     * @return array
     */
    protected function parseAnnotations($docblock)
    {
        $annotations = array();
        // Strip away the docblock header and footer
        // to ease parsing of one line annotations
        $docblock = \substr($docblock, 3, -2);
        $re = '/@(?P<name>[A-Za-z_-]+)(?:[ \\t]+(?P<value>.*?))?[ \\t]*\\r?$/m';
        if (\preg_match_all($re, $docblock, $matches)) {
            $numMatches = \count($matches[0]);
            for ($i = 0; $i < $numMatches; ++$i) {
                $annotations[$matches['name'][$i]][] = $matches['value'][$i];
            }
        }
        return $annotations;
    }
    /**
     * Log a message to the $logger object
     *
     * @param string $level   Logging level
     * @param string $message Text to log
     * @param array  $context Additional information
     *
     * @return null
     */
    protected function log($level, $message, array $context = array())
    {
        if ($this->logger) {
            $this->logger->log($level, $message, $context);
        }
    }
    /**
     * Sets a logger instance on the object
     *
     * @param LoggerInterface $logger PSR-3 compatible logger object
     *
     * @return null
     */
    public function setLogger($logger)
    {
        $this->logger = $logger;
    }
}
