Skip to content

Commit

Permalink
Merge branch 'NEXT-35992-many-to-one-data-validation' into 'trunk'
Browse files Browse the repository at this point in the history
NEXT-35992 - Many to one data validation

Closes NEXT-35992

See merge request shopware/6/product/platform!13783
  • Loading branch information
AydinHassan committed May 16, 2024
2 parents 3de8e0f + 59e96a5 commit 72b6b20
Show file tree
Hide file tree
Showing 5 changed files with 220 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,16 @@ public static function expectedArray(string $path): self
return new ExpectedArrayException($path);
}

public static function expectedAssociativeArray(string $path): self
{
return new self(
Response::HTTP_BAD_REQUEST,
self::INVALID_WRITE_INPUT,
'Expected data at {{ path }} to be an associative array.',
['path' => $path]
);
}

/**
* @deprecated tag:v6.7.0 - reason:return-type-change - Will only return `self` in the future
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,12 +98,13 @@ public function encode(
throw DataAbstractionLayerException::invalidSerializerField(ManyToOneAssociationField::class, $field);
}

if (!\is_array($data->getValue())) {
throw DataAbstractionLayerException::expectedArray($parameters->getPath());
$value = $data->getValue();
if (!\is_array($value) || !$this->isArrayAssociative($value)) {
throw DataAbstractionLayerException::expectedAssociativeArray($parameters->getPath());
}

$this->writeExtractor->extract(
$data->getValue(),
$value,
$parameters->cloneForSubresource(
$field->getReferenceDefinition(),
$parameters->getPath() . '/' . $data->getKey()
Expand All @@ -117,4 +118,14 @@ public function decode(Field $field, mixed $value): never
{
throw DataAbstractionLayerException::decodeHandledByHydrator($field);
}

/**
* @param array<array-key, mixed> $array
*/
private function isArrayAssociative(array $array): bool
{
$isString = array_map(is_string(...), array_keys($array));

return \count(array_filter($isString)) === \count($array);
}
}
1 change: 1 addition & 0 deletions src/Core/Framework/Resources/config/packages/shopware.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -505,6 +505,7 @@ shopware:
FRAMEWORK__INVALID_SALES_CHANNEL_MAPPING: notice
MAIL__MAIL_BODY_TOO_LONG: notice
CONTENT__MEDIA_MISSING_URL_PARAMETER: notice
FRAMEWORK__INVALID_WRITE_INPUT: notice

cache:
redis_prefix: '%env(REDIS_PREFIX)%'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,16 @@ public function testExpectedArray(): void
static::assertEquals('FRAMEWORK__WRITE_MALFORMED_INPUT', $e->getErrorCode());
}

public function testExpectedAssociativeArray(): void
{
$e = DataAbstractionLayerException::expectedAssociativeArray('some/path/0');

static::assertEquals('Expected data at some/path/0 to be an associative array.', $e->getMessage());
static::assertEquals('some/path/0', $e->getParameters()['path']);
static::assertEquals(Response::HTTP_BAD_REQUEST, $e->getStatusCode());
static::assertEquals('FRAMEWORK__INVALID_WRITE_INPUT', $e->getErrorCode());
}

public function testDecodeHandledByHydrator(): void
{
Feature::skipTestIfInActive('v6.7.0.0', $this);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
<?php declare(strict_types=1);

namespace Shopware\Tests\Unit\Core\Framework\DataAbstractionLayer\FieldSerializer;

use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
use Shopware\Core\Framework\Context;
use Shopware\Core\Framework\DataAbstractionLayer\DataAbstractionLayerException;
use Shopware\Core\Framework\DataAbstractionLayer\EntityDefinition;
use Shopware\Core\Framework\DataAbstractionLayer\Field\FkField;
use Shopware\Core\Framework\DataAbstractionLayer\Field\Flag\PrimaryKey;
use Shopware\Core\Framework\DataAbstractionLayer\Field\Flag\Required;
use Shopware\Core\Framework\DataAbstractionLayer\Field\IdField;
use Shopware\Core\Framework\DataAbstractionLayer\Field\ManyToOneAssociationField;
use Shopware\Core\Framework\DataAbstractionLayer\Field\OneToManyAssociationField;
use Shopware\Core\Framework\DataAbstractionLayer\Field\StringField;
use Shopware\Core\Framework\DataAbstractionLayer\FieldCollection;
use Shopware\Core\Framework\DataAbstractionLayer\FieldSerializer\ManyToOneAssociationFieldSerializer;
use Shopware\Core\Framework\DataAbstractionLayer\Write\Command\WriteCommandQueue;
use Shopware\Core\Framework\DataAbstractionLayer\Write\DataStack\KeyValuePair;
use Shopware\Core\Framework\DataAbstractionLayer\Write\EntityExistence;
use Shopware\Core\Framework\DataAbstractionLayer\Write\EntityWriteGatewayInterface;
use Shopware\Core\Framework\DataAbstractionLayer\Write\WriteCommandExtractor;
use Shopware\Core\Framework\DataAbstractionLayer\Write\WriteContext;
use Shopware\Core\Framework\DataAbstractionLayer\Write\WriteParameterBag;
use Shopware\Core\Framework\Uuid\Uuid;
use Shopware\Core\Test\Stub\DataAbstractionLayer\StaticDefinitionInstanceRegistry;
use Symfony\Component\Validator\Validator\ValidatorInterface;

/**
* @internal
*/
#[CoversClass(ManyToOneAssociationFieldSerializer::class)]
class ManyToOneAssociationFieldSerializerTest extends TestCase
{
/**
* @param array<array-key, mixed> $payload
*/
#[DataProvider('invalidArrayProvider')]
public function testExceptionIsThrownIfDataIsNotAssociativeArray(array $payload): void
{
$this->expectException(DataAbstractionLayerException::class);
static::expectExceptionMessage('Expected data at /customer to be an associative array.');

new StaticDefinitionInstanceRegistry(
[
OrderDefinition::class => $orderDefinition = new OrderDefinition(),
CustomerDefinition::class => new CustomerDefinition(),
],
$this->createMock(ValidatorInterface::class),
$this->createMock(EntityWriteGatewayInterface::class)
);

$field = $orderDefinition->getField('customer');

static::assertInstanceOf(ManyToOneAssociationField::class, $field);

$serializer = new ManyToOneAssociationFieldSerializer($this->createMock(WriteCommandExtractor::class));

$params = new WriteParameterBag(
$orderDefinition,
WriteContext::createFromContext(Context::createDefaultContext()),
'/customer',
new WriteCommandQueue()
);

$result = $serializer->encode(
$field,
$this->createMock(EntityExistence::class),
new KeyValuePair('customer', $payload, true),
$params
);

iterator_to_array($result);
}

public static function invalidArrayProvider(): \Generator
{
yield [
'payload' => ['should-be-an-associative-array'],
];

yield [
'payload' => [1 => 'apple', 'orange'],
];

yield [
'payload' => [0 => 'apple', 1 => 'orange'],
];

yield [
'payload' => [3 => 'apple', 5 => 'orange'],
];
}

public function testCanEncodeAssociativeArray(): void
{
new StaticDefinitionInstanceRegistry(
[
OrderDefinition::class => $orderDefinition = new OrderDefinition(),
CustomerDefinition::class => new CustomerDefinition(),
],
$this->createMock(ValidatorInterface::class),
$this->createMock(EntityWriteGatewayInterface::class)
);

$field = $orderDefinition->getField('customer');

static::assertInstanceOf(ManyToOneAssociationField::class, $field);

$serializer = new ManyToOneAssociationFieldSerializer($this->createMock(WriteCommandExtractor::class));

$params = new WriteParameterBag(
$orderDefinition,
WriteContext::createFromContext(Context::createDefaultContext()),
'/customer',
new WriteCommandQueue()
);

$id = Uuid::randomHex();

$result = $serializer->encode(
$field,
$this->createMock(EntityExistence::class),
new KeyValuePair('customer', ['id' => $id, 'name' => 'Jimmy'], true),
$params
);

static::assertEquals([], iterator_to_array($result));
}
}

/**
* @internal
*/
class OrderDefinition extends EntityDefinition
{
public function getEntityName(): string
{
return 'order';
}

protected function defineFields(): FieldCollection
{
return new FieldCollection([
(new IdField('id', 'id'))->addFlags(new Required(), new PrimaryKey()),
(new StringField('name', 'name'))->addFlags(new Required()),
new FkField('customer_id', 'customerId', CustomerDefinition::class),

new ManyToOneAssociationField(
'customer',
'customer_id',
CustomerDefinition::class,
'id',
),
]);
}
}

/**
* @internal
*/
class CustomerDefinition extends EntityDefinition
{
public function getEntityName(): string
{
return 'customer';
}

protected function defineFields(): FieldCollection
{
return new FieldCollection([
(new IdField('id', 'id'))->addFlags(new Required(), new PrimaryKey()),
(new StringField('first_name', 'first_name'))->addFlags(new Required()),
(new StringField('last_name', 'last_name'))->addFlags(new Required()),

new OneToManyAssociationField(
'orders',
OrderDefinition::class,
'customer_id',
),
]);
}
}

0 comments on commit 72b6b20

Please sign in to comment.