Skip to content
52 changes: 49 additions & 3 deletions lib/internal/Magento/Framework/Webapi/ServiceInputProcessor.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,16 @@
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
declare(strict_types=1);

namespace Magento\Framework\Webapi;

use Magento\Framework\Webapi\ServiceTypeToEntityTypeMap;
use Magento\Framework\Api\AttributeValue;
use Magento\Framework\Api\AttributeValueFactory;
use Magento\Framework\Api\SimpleDataObjectConverter;
use Magento\Framework\Exception\InputException;
use Magento\Framework\Exception\SerializationException;
use Magento\Framework\ObjectManager\ConfigInterface;
use Magento\Framework\ObjectManagerInterface;
use Magento\Framework\Phrase;
use Magento\Framework\Reflection\MethodsMap;
Expand Down Expand Up @@ -66,6 +67,11 @@ class ServiceInputProcessor implements ServicePayloadConverterInterface
*/
private $serviceTypeToEntityTypeMap;

/**
* @var ConfigInterface
*/
private $config;

/**
* Initialize dependencies.
*
Expand All @@ -75,14 +81,16 @@ class ServiceInputProcessor implements ServicePayloadConverterInterface
* @param CustomAttributeTypeLocatorInterface $customAttributeTypeLocator
* @param MethodsMap $methodsMap
* @param ServiceTypeToEntityTypeMap $serviceTypeToEntityTypeMap
* @param ConfigInterface $config
*/
public function __construct(
TypeProcessor $typeProcessor,
ObjectManagerInterface $objectManager,
AttributeValueFactory $attributeValueFactory,
CustomAttributeTypeLocatorInterface $customAttributeTypeLocator,
MethodsMap $methodsMap,
ServiceTypeToEntityTypeMap $serviceTypeToEntityTypeMap = null
ServiceTypeToEntityTypeMap $serviceTypeToEntityTypeMap = null,
ConfigInterface $config = null
) {
$this->typeProcessor = $typeProcessor;
$this->objectManager = $objectManager;
Expand All @@ -91,6 +99,8 @@ public function __construct(
$this->methodsMap = $methodsMap;
$this->serviceTypeToEntityTypeMap = $serviceTypeToEntityTypeMap
?: \Magento\Framework\App\ObjectManager::getInstance()->get(ServiceTypeToEntityTypeMap::class);
$this->config = $config
?: \Magento\Framework\App\ObjectManager::getInstance()->get(ConfigInterface::class);
}

/**
Expand Down Expand Up @@ -154,6 +164,33 @@ public function process($serviceClassName, $serviceMethodName, array $inputArray
return $inputData;
}

/**
* @param string $className
* @param array $data
* @return array
* @throws \ReflectionException
*/
private function getConstructorData(string $className, array $data): array
{
$preferenceClass = $this->config->getPreference($className);
$class = new ClassReflection($preferenceClass ?: $className);

$constructor = $class->getConstructor();
if ($constructor === null) {
return [];
}

$res = [];
$parameters = $constructor->getParameters();
foreach ($parameters as $parameter) {
if (isset($data[$parameter->getName()])) {
$res[$parameter->getName()] = $data[$parameter->getName()];
}
}

return $res;
}

/**
* Creates a new instance of the given class and populates it with the array of data. The data can
* be in different forms depending on the adapter being used, REST vs. SOAP. For REST, the data is
Expand All @@ -163,6 +200,7 @@ public function process($serviceClassName, $serviceMethodName, array $inputArray
* @param array $data
* @return object the newly created and populated object
* @throws \Exception
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
*/
protected function _createFromArray($className, $data)
{
Expand All @@ -174,9 +212,17 @@ protected function _createFromArray($className, $data)
if (is_subclass_of($className, self::EXTENSION_ATTRIBUTES_TYPE)) {
$className = substr($className, 0, -strlen('Interface'));
}
$object = $this->objectManager->create($className);

// Primary method: assign to constructor parameters
$constructorArgs = $this->getConstructorData($className, $data);
$object = $this->objectManager->create($className, $constructorArgs);

// Secondary method: fallback to setter methods
foreach ($data as $propertyName => $value) {
if (isset($constructorArgs[$propertyName])) {
continue;
}

// Converts snake_case to uppercase CamelCase to help form getter/setter method names
// This use case is for REST only. SOAP request data is already camel cased
$camelCaseProperty = SimpleDataObjectConverter::snakeCaseToUpperCamelCase($propertyName);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
declare(strict_types=1);

namespace Magento\Framework\Webapi\Test\Unit\ServiceInputProcessor;

class SimpleConstructor
{
/**
* @var int
*/
private $entityId;

/**
* @var string
*/
private $name;

public function __construct(
int $entityId,
string $name
) {
$this->entityId = $entityId;
$this->name = $name;
}

/**
* @return int|null
*/
public function getEntityId()
{
return $this->entityId;
}

/**
* @return string|null
*/
public function getName()
{
return $this->name;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,6 @@
*/
namespace Magento\Framework\Webapi\Test\Unit\ServiceInputProcessor;

use Magento\Framework\Webapi\Test\Unit\ServiceInputProcessor\AssociativeArray;
use Magento\Framework\Webapi\Test\Unit\ServiceInputProcessor\DataArray;
use Magento\Framework\Webapi\Test\Unit\ServiceInputProcessor\Nested;
use Magento\Framework\Webapi\Test\Unit\ServiceInputProcessor\SimpleArray;

class TestService
{
const DEFAULT_VALUE = 42;
Expand All @@ -25,6 +20,15 @@ public function simple($entityId, $name)
return [$entityId, $name];
}

/**
* @param \Magento\Framework\Webapi\Test\Unit\ServiceInputProcessor\SimpleConstructor $simpleConstructor
* @return \Magento\Framework\Webapi\Test\Unit\ServiceInputProcessor\SimpleConstructor
*/
public function simpleConstructor(SimpleConstructor $simpleConstructor)
{
return $simpleConstructor;
}

/**
* @param int $entityId
* @return string[]
Expand All @@ -34,6 +38,15 @@ public function simpleDefaultValue($entityId = self::DEFAULT_VALUE)
return [$entityId];
}

/**
* @param int $entityId
* @return string[]
*/
public function constructorArguments($entityId = self::DEFAULT_VALUE)
{
return [$entityId];
}

/**
* @param \Magento\Framework\Webapi\Test\Unit\ServiceInputProcessor\Nested $nested
* @return \Magento\Framework\Webapi\Test\Unit\ServiceInputProcessor\Nested
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use Magento\Framework\Serialize\SerializerInterface;
use Magento\Framework\Webapi\ServiceInputProcessor;
use Magento\Framework\Webapi\ServiceTypeToEntityTypeMap;
use Magento\Framework\Webapi\Test\Unit\ServiceInputProcessor\SimpleConstructor;
use Magento\Framework\Webapi\Test\Unit\ServiceInputProcessor\WebapiBuilderFactory;
use Magento\Framework\Webapi\Test\Unit\ServiceInputProcessor\AssociativeArray;
use Magento\Framework\Webapi\Test\Unit\ServiceInputProcessor\DataArray;
Expand Down Expand Up @@ -59,8 +60,8 @@ protected function setUp()
$this->objectManagerMock->expects($this->any())
->method('create')
->willReturnCallback(
function ($className) use ($objectManager) {
return $objectManager->getObject($className);
function ($className, $arguments = []) use ($objectManager) {
return $objectManager->getObject($className, $arguments);
}
);

Expand Down Expand Up @@ -202,6 +203,22 @@ public function testNestedDataProperties()
$this->assertEquals('Test', $details->getName());
}

public function testSimpleConstructorProperties()
{
$data = ['simpleConstructor' => ['entityId' => 15, 'name' => 'Test']];
$result = $this->serviceInputProcessor->process(
\Magento\Framework\Webapi\Test\Unit\ServiceInputProcessor\TestService::class,
'simpleConstructor',
$data
);
$this->assertNotNull($result);
$arg = $result[0];

$this->assertTrue($arg instanceof SimpleConstructor);
$this->assertEquals(15, $arg->getEntityId());
$this->assertEquals('Test', $arg->getName());
}

public function testSimpleArrayProperties()
{
$data = ['ids' => [1, 2, 3, 4]];
Expand Down