Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,54 @@

The goal of this Plugin is to ease the pain of dealing with relations in Shopware.

## Shopware native

Before explaining the solution in this Plugin, please be aware that there is a Shopware native way in the Sync-API
to archive the same result. For example to assign a new category, you can send the following HTTP request:

```http request
POST /api/_action/sync
Content-Type: application/json

{
"change-category": {
"entity": "product",
"action": "upsert",
"payload": [
{
"id": "<the product id>",
"categories": [
{"id": "<the new category id 1>"}
{"id": "<the new category id 2>"}
]
}
]
},
"delete-obsolete": {
"entity": "product_category",
"action": "delete",
"criteria": [
{
"type": "equals",
"field": "productId",
"value": "<the product id>"
},
{
"type": "not",
"operator": "and",
"queries": [
{
"type": "equalsAny",
"field": "categoryId",
"value": ["<the new category id 1>", "<the new category id 2>"]
}
]
}
]
}
}
```

## Quick start

After installing the Plugin, you can enable automatic relation cleanup in
Expand Down
10 changes: 9 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"shopware/core": "~6.6.4"
},
"require-dev": {
"de-swebhosting-shopware-plugin/smart-relation-sync-test-plugin": "1.0.0",
"de-swebhosting/php-codestyle": "^5.4",
"ergebnis/composer-normalize": "^2.45",
"friendsofphp/php-cs-fixer": "^3.66",
Expand All @@ -19,14 +20,21 @@
"shopware/dev-tools": "^1.5",
"symplify/phpstan-rules": "^14.4"
},
"repositories": {
"SmartRelationSyncTestPlugin": {
"type": "path",
"url": "./tests/Fixtures/SmartRelationSyncTestPlugin"
}
},
"autoload": {
"psr-4": {
"Swh\\SmartRelationSync\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Swh\\SmartRelationSync\\Tests\\": "tests/"
"Swh\\SmartRelationSync\\Tests\\Compatibility\\": "tests/Compatibility/",
"Swh\\SmartRelationSync\\Tests\\Functional\\": "tests/Functional/"
}
},
"config": {
Expand Down
13 changes: 13 additions & 0 deletions phpstan.neon
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,19 @@ parameters:
consoleApplicationLoader: tests/StaticAnalyze/PHPStan/console-application.php
type_perfect:
no_mixed: true
ignoreErrors:
- identifier: class.extendsInternalClass
- identifier: classConstant.internalClass
- identifier: method.internal
- identifier: method.internalClass
- identifier: method.internalInterface
- identifier: new.internalClass
- identifier: parameter.internalClass
- identifier: parameter.internalInterface
- identifier: property.internalClass
- identifier: property.internalInterface
- identifier: return.internalClass
- identifier: staticMethod.internal

services:
- # register the class, so we can decorate it, but don't tag it as a rule, so only our decorator is used by PHPStan
Expand Down
13 changes: 2 additions & 11 deletions src/ApiDefinition/EntitySchemaGeneratorDecorator.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,9 @@
use Shopware\Core\Framework\Api\Context\AdminApiSource;
use Shopware\Core\Framework\DataAbstractionLayer\EntityDefinition;
use Shopware\Core\Framework\DataAbstractionLayer\Field\Field;
use Shopware\Core\Framework\DataAbstractionLayer\Field\ManyToManyAssociationField;
use Shopware\Core\Framework\DataAbstractionLayer\Field\OneToManyAssociationField;
use Shopware\Core\System\SalesChannel\Entity\SalesChannelDefinitionInterface;
use Swh\SmartRelationSync\DataAbstractionLayer\WriteCommandExtractorDecorator;
use Swh\SmartRelationSync\ValueObject\RelevantField;

/**
* @internal
Expand Down Expand Up @@ -66,7 +65,7 @@ public function getSchema(array $definitions): array
$entityProperties = $schema[$entity]['properties'];

$relevantFields = $definition->getFields()
->filter(fn(Field $field) => $this->isRelevantField($field));
->filter(static fn(Field $field) => RelevantField::isRelevant($field));

foreach ($relevantFields as $field) {
if (!array_key_exists($field->getPropertyName(), $entityProperties)) {
Expand All @@ -88,12 +87,4 @@ public function supports(string $format, string $api): bool
{
return $this->decorated->supports($format, $api);
}

/**
* @phpstan-assert-if-true ManyToManyAssociationField|OneToManyAssociationField $field
*/
private function isRelevantField(Field $field): bool
{
return WriteCommandExtractorDecorator::isRelevantField($field);
}
}
55 changes: 41 additions & 14 deletions src/ApiDefinition/OpenApiDefinitionSchemaBuilderDecorator.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,18 @@

use OpenApi\Annotations\Property;
use OpenApi\Annotations\Schema;
use RuntimeException;
use Shopware\Core\Framework\Api\ApiDefinition\DefinitionService;
use Shopware\Core\Framework\Api\ApiDefinition\Generator\OpenApi\OpenApiDefinitionSchemaBuilder;
use Shopware\Core\Framework\Api\Context\AdminApiSource;
use Shopware\Core\Framework\Api\Context\SalesChannelApiSource;
use Shopware\Core\Framework\DataAbstractionLayer\EntityDefinition;
use Shopware\Core\Framework\DataAbstractionLayer\Field\Field;
use Shopware\Core\Framework\DataAbstractionLayer\Field\Flag\ApiAware;
use Shopware\Core\Framework\DataAbstractionLayer\Field\Flag\Extension;
use Shopware\Core\Framework\DataAbstractionLayer\Field\Flag\IgnoreInOpenapiSchema;
use Shopware\Core\Framework\DataAbstractionLayer\Field\ManyToManyAssociationField;
use Shopware\Core\Framework\DataAbstractionLayer\Field\OneToManyAssociationField;
use Swh\SmartRelationSync\DataAbstractionLayer\WriteCommandExtractorDecorator;
use Swh\SmartRelationSync\ValueObject\RelevantField;
use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter;

class OpenApiDefinitionSchemaBuilderDecorator extends OpenApiDefinitionSchemaBuilder
Expand Down Expand Up @@ -53,7 +54,7 @@ public function getSchemaByDefinition(
}

$relevantFields = $definition->getFields()
->filter(fn(Field $field) => $this->isRelevantField($field));
->filter(static fn(Field $field) => RelevantField::isRelevant($field));

foreach ($relevantFields as $field) {
if (!$this->shouldFieldBeIncluded($field, $forSalesChannel)) {
Expand All @@ -68,14 +69,48 @@ public function getSchemaByDefinition(
'writeOnly' => true,
]);

foreach ($relevantSchemas as $schema) {
$schema->properties[] = $property;
}
match ($field->is(Extension::class)) {
true => $this->addExtensionToSchemas($relevantSchemas, $property),
false => $this->addPropertyToSchemas($relevantSchemas, $property),
};
}

return $schemas;
}

/**
* @param Schema[] $schemas
*/
private function addExtensionToSchemas(array $schemas, Property $property): void
{
foreach ($schemas as $schema) {
$extensionSchema = $this->getExtensionSchema($schema);

$extensionSchema->properties[] = $property;
}
}

/**
* @param Schema[] $schemas
*/
private function addPropertyToSchemas(array $schemas, Property $property): void
{
foreach ($schemas as $schema) {
$schema->properties[] = $property;
}
}

private function getExtensionSchema(Schema $schema): Schema
{
foreach ($schema->properties as $property) {
if ($property->property === 'extensions') {
return $property;
}
}

throw new RuntimeException('extensions property not found');
}

/**
* @param Schema[] $schemas
*
Expand Down Expand Up @@ -108,14 +143,6 @@ private function getRelevantSchemas(array $schemas, EntityDefinition $definition
return $relevantSchemas;
}

/**
* @phpstan-assert-if-true ManyToManyAssociationField|OneToManyAssociationField $field
*/
private function isRelevantField(Field $field): bool
{
return WriteCommandExtractorDecorator::isRelevantField($field);
}

private function shouldFieldBeIncluded(Field $field, bool $forSalesChannel): bool
{
if ($field->getPropertyName() === 'translations'
Expand Down
101 changes: 69 additions & 32 deletions src/DataAbstractionLayer/WriteCommandExtractorDecorator.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use Shopware\Core\Framework\DataAbstractionLayer\EntityDefinition;
use Shopware\Core\Framework\DataAbstractionLayer\Field\Field;
use Shopware\Core\Framework\DataAbstractionLayer\Field\FkField;
use Shopware\Core\Framework\DataAbstractionLayer\Field\Flag\Extension;
use Shopware\Core\Framework\DataAbstractionLayer\Field\ManyToManyAssociationField;
use Shopware\Core\Framework\DataAbstractionLayer\Field\ManyToOneAssociationField;
use Shopware\Core\Framework\DataAbstractionLayer\Field\OneToManyAssociationField;
Expand All @@ -17,6 +18,7 @@
use Shopware\Core\Framework\DataAbstractionLayer\Version\VersionDefinition;
use Shopware\Core\Framework\DataAbstractionLayer\Write\WriteCommandExtractor;
use Shopware\Core\Framework\DataAbstractionLayer\Write\WriteParameterBag;
use Swh\SmartRelationSync\ValueObject\RelevantField;

final class WriteCommandExtractorDecorator extends WriteCommandExtractor
{
Expand All @@ -31,15 +33,6 @@ public static function getCleanupEnableFieldName(Field $field): string
return sprintf('%sCleanupRelations', $field->getPropertyName());
}

/**
* @phpstan-assert-if-true ManyToManyAssociationField|OneToManyAssociationField $field
*/
public static function isRelevantField(Field $field): bool
{
return $field instanceof ManyToManyAssociationField
|| $field instanceof OneToManyAssociationField;
}

/**
* @param array<mixed, mixed> $rawData
*/
Expand Down Expand Up @@ -226,6 +219,65 @@ private function getPrimaryKeyFields(EntityDefinition $reference): array
return $fields;
}

/**
* @param array<mixed, mixed> $rawData
*
* @return array<mixed, mixed>
*/
private function getRelevantRawData(Field $field, array $rawData): array
{
if (!$field->is(Extension::class)) {
return $rawData;
}

$propertyName = $field->getPropertyName();

if (isset($rawData[$propertyName])) {
return $rawData;
}

if (
!isset($rawData['extensions'])
|| !is_array($rawData['extensions'])
|| !isset($rawData['extensions'][$propertyName])) {
return $rawData;
}

return $rawData['extensions'];
}

/**
* @param array<mixed, mixed> $rawData
*/
private function registerFieldForCleanup(
RelevantField $relevantField,
array $rawData,
): void {
$field = $relevantField->field;

$fieldData = $rawData[$field->getPropertyName()] ?? null;

if (!is_array($fieldData)) {
return;
}

$cleanupEnableField = $this->getCleanupEnableFieldName($field);

if (!array_key_exists($cleanupEnableField, $rawData)) {
return;
}

$cleanupEnabled = is_bool($rawData[$cleanupEnableField]) && $rawData[$cleanupEnableField];

unset($rawData[$cleanupEnableField]);

if (!$cleanupEnabled) {
return;
}

$this->registerRelationsForField($relevantField, $fieldData);
}

/**
* @param array<mixed, mixed> $rawData
*/
Expand All @@ -247,43 +299,28 @@ private function registerRelations(array $rawData, WriteParameterBag $parameters
$primaryKeys = $this->filterVersionFields($primaryKeys, $definition);

foreach ($definition->getFields() as $field) {
if (!self::isRelevantField($field)) {
continue;
}

$cleanupEnableField = $this->getCleanupEnableFieldName($field);
$relevantField = RelevantField::create($field, $primaryKeys);

if (!array_key_exists($cleanupEnableField, $rawData)) {
if ($relevantField === null) {
continue;
}

$cleanupEnabled = is_bool($rawData[$cleanupEnableField]) && $rawData[$cleanupEnableField];

unset($rawData[$cleanupEnableField]);

if (!$cleanupEnabled) {
continue; // @codeCoverageIgnore
}

$fieldData = $rawData[$field->getPropertyName()] ?? null;
$rawData = $this->getRelevantRawData($field, $rawData);

if (!is_array($fieldData)) {
continue;
}

$this->registerRelationsForField($field, $primaryKeys, $fieldData);
$this->registerFieldForCleanup($relevantField, $rawData);
}
}

/**
* @param non-empty-array<non-empty-string, non-empty-string> $parentPrimaryKey
* @param array<mixed, mixed> $fieldData
*/
private function registerRelationsForField(
ManyToManyAssociationField|OneToManyAssociationField $field,
array $parentPrimaryKey,
RelevantField $relevantField,
array $fieldData,
): void {
$field = $relevantField->field;
$parentPrimaryKey = $relevantField->parentPrimaryKey;

$reference = $field->getReferenceDefinition();

$cleanupRelationData = match ($field instanceof OneToManyAssociationField) {
Expand Down
Loading