Skip to content

Commit

Permalink
Merge branch 'next-25734/sync-api-improvement' into 'trunk'
Browse files Browse the repository at this point in the history
NEXT-25734 - Added option to provide a criteria inside the sync operations in...

See merge request shopware/6/product/platform!10633
  • Loading branch information
OliverSkroblin committed May 30, 2023
2 parents 0d12bb2 + cc5c28a commit 30e574c
Show file tree
Hide file tree
Showing 17 changed files with 752 additions and 14 deletions.
5 changes: 0 additions & 5 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -7444,11 +7444,6 @@ parameters:
count: 1
path: src/Core/Framework/Api/Controller/SalesChannelProxyController.php

-
message: "#^Parameter \\#2 \\$skipIndexers of class Shopware\\\\Core\\\\Framework\\\\Api\\\\Sync\\\\SyncBehavior constructor expects list\\<string\\>, array\\<int\\<0, max\\>, non\\-falsy\\-string\\> given\\.$#"
count: 1
path: src/Core/Framework/Api/Controller/SyncController.php

-
message: "#^Anonymous variable in a `\\$event\\-\\>\\.\\.\\.\\(\\)` method call can lead to false dead methods\\. Make sure the variable type is known$#"
count: 2
Expand Down
5 changes: 5 additions & 0 deletions src/Core/Content/DependencyInjection/product.xml
Original file line number Diff line number Diff line change
Expand Up @@ -508,5 +508,10 @@
</service>

<service id="Shopware\Core\Content\Product\SalesChannel\ProductCloseoutFilterFactory"/>

<service id="Shopware\Core\Content\Product\Api\ProductNumberFkResolver">
<argument type="service" id="Doctrine\DBAL\Connection"/>
<tag name="shopware.sync.fk_resolver"/>
</service>
</services>
</container>
1 change: 0 additions & 1 deletion src/Core/Content/DependencyInjection/property.xml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,5 @@
<service id="Shopware\Core\Content\Property\Aggregate\PropertyGroupTranslation\PropertyGroupTranslationDefinition">
<tag name="shopware.entity.definition"/>
</service>

</services>
</container>
55 changes: 55 additions & 0 deletions src/Core/Content/Product/Api/ProductNumberFkResolver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?php declare(strict_types=1);

namespace Shopware\Core\Content\Product\Api;

use Doctrine\DBAL\ArrayParameterType;
use Doctrine\DBAL\Connection;
use Shopware\Core\Defaults;
use Shopware\Core\Framework\Api\Sync\AbstractFkResolver;
use Shopware\Core\Framework\Api\Sync\FkReference;
use Shopware\Core\Framework\Log\Package;
use Shopware\Core\Framework\Uuid\Uuid;

/**
* @internal
*/
#[Package('core')]
class ProductNumberFkResolver extends AbstractFkResolver
{
public function __construct(private readonly Connection $connection)
{
}

public static function getName(): string
{
return 'product.number';
}

/**
* @param array<FkReference> $map
*
* @return array<FkReference>
*/
public function resolve(array $map): array
{
$numbers = \array_map(fn ($id) => $id->value, $map);

$numbers = \array_filter(\array_unique($numbers));

if (empty($numbers)) {
return $map;
}

$hash = $this->connection->fetchAllKeyValue(
'SELECT product_number, LOWER(HEX(id)) FROM product WHERE product_number IN (:numbers) AND version_id = :version',
['numbers' => $numbers, 'version' => Uuid::fromHexToBytes(Defaults::LIVE_VERSION)],
['numbers' => ArrayParameterType::STRING]
);

foreach ($map as $reference) {
$reference->resolved = $hash[$reference->value];
}

return $map;
}
}
42 changes: 42 additions & 0 deletions src/Core/Framework/Api/ApiException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php declare(strict_types=1);

namespace Shopware\Core\Framework\Api;

use Shopware\Core\Framework\HttpException;
use Shopware\Core\Framework\Log\Package;
use Symfony\Component\HttpFoundation\Response;

#[Package('core')]
class ApiException extends HttpException
{
public const API_INVALID_SYNC_CRITERIA_EXCEPTION = 'API_INVALID_SYNC_CRITERIA_EXCEPTION';
public const API_RESOLVER_NOT_FOUND_EXCEPTION = 'API_RESOLVER_NOT_FOUND_EXCEPTION';
public const API_INVALID_SYNC_OPERATION_EXCEPTION = 'FRAMEWORK__INVALID_SYNC_OPERATION';

public static function invalidSyncCriteriaException(string $operationKey): self
{
return new self(
Response::HTTP_BAD_REQUEST,
self::API_INVALID_SYNC_CRITERIA_EXCEPTION,
\sprintf('Sync operation %s, with action "delete", requires a criteria with at least one filter and can only be applied for mapping entities', $operationKey)
);
}

public static function invalidSyncOperationException(string $message): self
{
return new self(
Response::HTTP_BAD_REQUEST,
self::API_INVALID_SYNC_OPERATION_EXCEPTION,
$message
);
}

public static function resolverNotFoundException(string $key): self
{
return new self(
Response::HTTP_BAD_REQUEST,
self::API_RESOLVER_NOT_FOUND_EXCEPTION,
\sprintf('Foreign key resolver for key %s not found', $key)
);
}
}
15 changes: 14 additions & 1 deletion src/Core/Framework/Api/Controller/SyncController.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Shopware\Core\Framework\Api\Controller;

use Doctrine\DBAL\ConnectionException;
use Shopware\Core\Framework\Api\ApiException;
use Shopware\Core\Framework\Api\Exception\InvalidSyncOperationException;
use Shopware\Core\Framework\Api\Sync\SyncBehavior;
use Shopware\Core\Framework\Api\Sync\SyncOperation;
Expand Down Expand Up @@ -41,6 +42,7 @@ public function __construct(
#[Route(path: '/api/_action/sync', name: 'api.action.sync', methods: ['POST'])]
public function sync(Request $request, Context $context): JsonResponse
{
/** @var list<string> $indexingSkips */
$indexingSkips = array_filter(explode(',', (string) $request->headers->get(PlatformRequest::HEADER_INDEXING_SKIP, '')));

$behavior = new SyncBehavior(
Expand All @@ -55,7 +57,18 @@ public function sync(Request $request, Context $context): JsonResponse
if (isset($operation['key'])) {
$key = $operation['key'];
}
$operations[] = new SyncOperation((string) $key, $operation['entity'], $operation['action'], $operation['payload']);
$key = (string) $key;
$operations[] = new SyncOperation(
$key,
$operation['entity'],
$operation['action'],
$operation['payload'] ?? [],
$operation['criteria'] ?? []
);

if (empty($operation['entity'])) {
throw ApiException::invalidSyncOperationException(sprintf('Missing "entity" argument for operation with key "%s". It needs to be a non-empty string.', (string) $key));
}
}

$result = $context->scope(Context::CRUD_API_SCOPE, fn (Context $context): SyncResult => $this->syncService->sync($operations, $context, $behavior));
Expand Down
21 changes: 21 additions & 0 deletions src/Core/Framework/Api/Sync/AbstractFkResolver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php declare(strict_types=1);

namespace Shopware\Core\Framework\Api\Sync;

use Shopware\Core\Framework\Log\Package;

#[Package('core')]
abstract class AbstractFkResolver
{
/**
* Returns the unique name for the resolver which is used to identify for fk resolving hash map
*/
abstract public static function getName(): string;

/**
* @param array<FkReference> $map
*
* @return array<FkReference>
*/
abstract public function resolve(array $map): array;
}
18 changes: 18 additions & 0 deletions src/Core/Framework/Api/Sync/FkReference.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php declare(strict_types=1);

namespace Shopware\Core\Framework\Api\Sync;

use Shopware\Core\Framework\Log\Package;

/**
* @final
*/
#[Package('core')]
class FkReference
{
public ?string $resolved = null;

public function __construct(public mixed $value)
{
}
}
143 changes: 143 additions & 0 deletions src/Core/Framework/Api/Sync/SyncFkResolver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
<?php declare(strict_types=1);

namespace Shopware\Core\Framework\Api\Sync;

use Shopware\Core\Framework\Api\ApiException;
use Shopware\Core\Framework\DataAbstractionLayer\DefinitionInstanceRegistry;
use Shopware\Core\Framework\DataAbstractionLayer\Field\AssociationField;
use Shopware\Core\Framework\DataAbstractionLayer\Field\FkField;
use Shopware\Core\Framework\DataAbstractionLayer\Field\IdField;
use Shopware\Core\Framework\DataAbstractionLayer\Field\ManyToManyAssociationField;
use Shopware\Core\Framework\DataAbstractionLayer\Field\ManyToOneAssociationField;
use Shopware\Core\Framework\DataAbstractionLayer\Field\OneToManyAssociationField;
use Shopware\Core\Framework\DataAbstractionLayer\Field\OneToOneAssociationField;
use Shopware\Core\Framework\Log\Package;

/**
* @internal
*/
#[Package('core')]
class SyncFkResolver
{
/**
* @internal
*
* @param iterable<AbstractFkResolver> $resolvers
*/
public function __construct(
private readonly DefinitionInstanceRegistry $registry,
private readonly iterable $resolvers
) {
}

/**
* @param array<int, array<string, mixed>> $payload
*
* @return array<int, array<string, mixed>>
*/
public function resolve(string $entity, array $payload): array
{
$map = $this->collect($entity, $payload);

if (empty($map)) {
return $payload;
}

foreach ($map as $key => &$values) {
$values = $this->getResolver($key)->resolve($values);
}

\array_walk_recursive($payload, function (&$value): void {
$value = $value instanceof FkReference ? $value->resolved : $value;
});

return $payload;
}

/**
* @param array<int, array<string, mixed>> $payload
*
* @return array<string, array<FkReference>>
*/
private function collect(string $entity, array &$payload): array
{
$definition = $this->registry->getByEntityName($entity);

$map = [];
foreach ($payload as &$row) {
foreach ($row as $key => &$value) {
if (\is_array($value) && isset($value['resolver']) && isset($value['value'])) {
$definition = $this->registry->getByEntityName($entity);

$field = $definition->getField($key);

$ref = match (true) {
$field instanceof FkField => $field->getReferenceDefinition()->getEntityName(),
$field instanceof IdField => $entity,
default => null
};

if ($ref === null) {
continue;
}

$resolver = (string) $value['resolver'];

$row[$key] = $reference = new FkReference($value['value']);

$map[$resolver][] = $reference;
}

if (\is_array($value)) {
$field = $definition->getField($key);

if (!$field instanceof AssociationField) {
continue;
}

$nested = [];
if ($field instanceof ManyToManyAssociationField || $field instanceof OneToManyAssociationField) {
$ref = $field instanceof ManyToManyAssociationField ? $field->getToManyReferenceDefinition()->getEntityName() : $field->getReferenceDefinition()->getEntityName();
$nested = $this->collect($ref, $value);
} elseif ($field instanceof ManyToOneAssociationField || $field instanceof OneToOneAssociationField) {
$tmp = [$value];
$nested = $this->collect($field->getReferenceDefinition()->getEntityName(), $tmp);
$value = \array_shift($tmp);
}

$map = $this->merge($map, $nested);
}
}
}

return $map;
}

/**
* @param array<string, array<FkReference>> $map
* @param array<string, array<FkReference>> $nested
*
* @return array<string, array<FkReference>>
*/
private function merge(array $map, array $nested): array
{
foreach ($nested as $resolver => $values) {
foreach ($values as $value) {
$map[$resolver][] = $value;
}
}

return $map;
}

private function getResolver(string $key): AbstractFkResolver
{
foreach ($this->resolvers as $resolver) {
if ($resolver::getName() === $key) {
return $resolver;
}
}

throw ApiException::resolverNotFoundException($key);
}
}
27 changes: 26 additions & 1 deletion src/Core/Framework/Api/Sync/SyncOperation.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,14 @@ class SyncOperation extends Struct

/**
* @param array<int, mixed> $payload
* @param array<int, mixed> $criteria
*/
public function __construct(
protected string $key,
protected string $entity,
protected string $action,
protected array $payload
protected array $payload,
protected array $criteria = []
) {
}

Expand Down Expand Up @@ -89,4 +91,27 @@ public function validate(): array

return $errors;
}

/**
* @internal used to replace payload in case of api shorthands (e.g. delete mappings with wild cards, etc)
*
* @param array<int, mixed> $payload
*/
public function replacePayload(array $payload): void
{
$this->payload = $payload;
}

/**
* @return array<int, mixed> $criteria
*/
public function getCriteria(): array
{
return $this->criteria;
}

public function hasCriteria(): bool
{
return !empty($this->criteria);
}
}
Loading

0 comments on commit 30e574c

Please sign in to comment.