<?php /** * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ namespace Magento\Webapi\Controller\Rest; use Magento\Framework\Webapi\Rest\Request\ParamOverriderInterface; use Magento\Webapi\Model\Config\Converter; use Magento\Framework\Reflection\MethodsMap; use Magento\Framework\Api\SimpleDataObjectConverter; /** * Override parameter values */ class ParamsOverrider { /** * @var ParamOverriderInterface[] */ private $paramOverriders; /** * @var MethodsMap */ private $methodsMap; /** * Initialize dependencies * * @param ParamOverriderInterface[] $paramOverriders */ public function __construct( array $paramOverriders = [] ) { $this->paramOverriders = $paramOverriders; } /** * Override parameter values based on webapi.xml * * @param array $inputData Incoming data from request * @param array $parameters Contains parameters to replace or default * @return array Data in same format as $inputData with appropriate parameters added or changed */ public function override(array $inputData, array $parameters) { foreach ($parameters as $name => $paramData) { $arrayKeys = explode('.', $name); if ($paramData[Converter::KEY_FORCE] || !$this->isNestedArrayValueSet($inputData, $arrayKeys)) { $paramValue = $paramData[Converter::KEY_VALUE]; if (isset($this->paramOverriders[$paramValue])) { $value = $this->paramOverriders[$paramValue]->getOverriddenValue(); } else { $value = $paramData[Converter::KEY_VALUE]; } $this->setNestedArrayValue($inputData, $arrayKeys, $value); } } return $inputData; } /** * Determine if a nested array value is set. * * @param array &$nestedArray * @param string[] $arrayKeys * @return bool true if array value is set */ protected function isNestedArrayValueSet(&$nestedArray, $arrayKeys) { $currentArray = &$nestedArray; foreach ($arrayKeys as $key) { if (!isset($currentArray[$key])) { return false; } $currentArray = &$currentArray[$key]; } return true; } /** * Set a nested array value. * * @param array &$nestedArray * @param string[] $arrayKeys * @param string $valueToSet * @return void */ protected function setNestedArrayValue(&$nestedArray, $arrayKeys, $valueToSet) { $currentArray = &$nestedArray; $lastKey = array_pop($arrayKeys); foreach ($arrayKeys as $key) { if (!isset($currentArray[$key])) { $currentArray[$key] = []; } $currentArray = &$currentArray[$key]; } $currentArray[$lastKey] = $valueToSet; } /** * Override request body property value with matching url path parameter value * * This method assumes that webapi.xml url defines the substitution parameter as camelCase to the actual * snake case key described as part of the api contract. example: /:parentId/nestedResource/:entityId. * Here :entityId value will be used for overriding 'entity_id' property in the body. * Since Webapi framework allows both camelCase and snakeCase, either of them will be substituted for now. * If the request body is missing url path parameter as property, it will be added to the body. * This method works only requests with scalar properties at top level or properties of single object embedded * in the request body. * Only the last path parameter value will be substituted from the url in case of multiple parameters. * * @param array $urlPathParams url path parameters as array * @param array $requestBodyParams body parameters as array * @param string $serviceClassName name of the service class that we are trying to call * @param string $serviceMethodName name of the method that we are trying to call * @return array */ public function overrideRequestBodyIdWithPathParam( array $urlPathParams, array $requestBodyParams, $serviceClassName, $serviceMethodName ) { if (empty($urlPathParams)) { return $requestBodyParams; } $pathParamValue = end($urlPathParams); // Self apis should not be overridden if ($pathParamValue === 'me') { return $requestBodyParams; } $pathParamKey = key($urlPathParams); // Check if the request data is a top level object of body if (count($requestBodyParams) == 1 && is_array(end($requestBodyParams))) { $requestDataKey = key($requestBodyParams); if ($this->isPropertyDeclaredInDataObject( $serviceClassName, $serviceMethodName, $requestDataKey, $pathParamKey ) ) { $this->substituteParameters($requestBodyParams[$requestDataKey], $pathParamKey, $pathParamValue); } else { $this->substituteParameters($requestBodyParams, $pathParamKey, $pathParamValue); } } else { // Else parameters passed as scalar values in body will be overridden $this->substituteParameters($requestBodyParams, $pathParamKey, $pathParamValue); } return $requestBodyParams; } /** * Check presence for both camelCase and snake_case keys in array and substitute if either is present * * @param array $requestData * @param string $key * @param string $value * @return void */ private function substituteParameters(array &$requestData, $key, $value) { $snakeCaseKey = SimpleDataObjectConverter::camelCaseToSnakeCase($key); $camelCaseKey = SimpleDataObjectConverter::snakeCaseToCamelCase($key); if (isset($requestData[$camelCaseKey])) { $requestData[$camelCaseKey] = $value; } else { $requestData[$snakeCaseKey] = $value; } } /** * Verify property in parameter's object * * @param string $serviceClassName name of the service class that we are trying to call * @param string $serviceMethodName name of the method that we are trying to call * @param string $serviceMethodParamName * @param string $objectProperty * @return bool */ private function isPropertyDeclaredInDataObject( $serviceClassName, $serviceMethodName, $serviceMethodParamName, $objectProperty ) { if ($serviceClassName && $serviceMethodName) { $methodParams = $this->getMethodsMap()->getMethodParams($serviceClassName, $serviceMethodName); $index = array_search($serviceMethodParamName, array_column($methodParams, 'name')); if ($index !== false) { $paramObjectType = $methodParams[$index][MethodsMap::METHOD_META_TYPE]; $setter = 'set' . SimpleDataObjectConverter::snakeCaseToUpperCamelCase($objectProperty); if (array_key_exists( $setter, $this->getMethodsMap()->getMethodsMap($paramObjectType) )) { return true; } } } return false; } /** * The getter function to get MethodsMap object * * @return \Magento\Framework\Reflection\MethodsMap * * @deprecated 100.1.0 */ private function getMethodsMap() { if ($this->methodsMap === null) { $this->methodsMap = \Magento\Framework\App\ObjectManager::getInstance() ->get(MethodsMap::class); } return $this->methodsMap; } }