Skip to content

Commit 2754d25

Browse files
authored
feat: support extensions key in DAL operations (#5)
Also: Document Shopware native solution in README Ignore new PHPStan internal errors
1 parent 64b4cb9 commit 2754d25

19 files changed

+509
-71
lines changed

README.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,54 @@
22

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

5+
## Shopware native
6+
7+
Before explaining the solution in this Plugin, please be aware that there is a Shopware native way in the Sync-API
8+
to archive the same result. For example to assign a new category, you can send the following HTTP request:
9+
10+
```http request
11+
POST /api/_action/sync
12+
Content-Type: application/json
13+
14+
{
15+
"change-category": {
16+
"entity": "product",
17+
"action": "upsert",
18+
"payload": [
19+
{
20+
"id": "<the product id>",
21+
"categories": [
22+
{"id": "<the new category id 1>"}
23+
{"id": "<the new category id 2>"}
24+
]
25+
}
26+
]
27+
},
28+
"delete-obsolete": {
29+
"entity": "product_category",
30+
"action": "delete",
31+
"criteria": [
32+
{
33+
"type": "equals",
34+
"field": "productId",
35+
"value": "<the product id>"
36+
},
37+
{
38+
"type": "not",
39+
"operator": "and",
40+
"queries": [
41+
{
42+
"type": "equalsAny",
43+
"field": "categoryId",
44+
"value": ["<the new category id 1>", "<the new category id 2>"]
45+
}
46+
]
47+
}
48+
]
49+
}
50+
}
51+
```
52+
553
## Quick start
654

755
After installing the Plugin, you can enable automatic relation cleanup in

composer.json

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"shopware/core": "~6.6.4"
88
},
99
"require-dev": {
10+
"de-swebhosting-shopware-plugin/smart-relation-sync-test-plugin": "1.0.0",
1011
"de-swebhosting/php-codestyle": "^5.4",
1112
"ergebnis/composer-normalize": "^2.45",
1213
"friendsofphp/php-cs-fixer": "^3.66",
@@ -19,14 +20,21 @@
1920
"shopware/dev-tools": "^1.5",
2021
"symplify/phpstan-rules": "^14.4"
2122
},
23+
"repositories": {
24+
"SmartRelationSyncTestPlugin": {
25+
"type": "path",
26+
"url": "./tests/Fixtures/SmartRelationSyncTestPlugin"
27+
}
28+
},
2229
"autoload": {
2330
"psr-4": {
2431
"Swh\\SmartRelationSync\\": "src/"
2532
}
2633
},
2734
"autoload-dev": {
2835
"psr-4": {
29-
"Swh\\SmartRelationSync\\Tests\\": "tests/"
36+
"Swh\\SmartRelationSync\\Tests\\Compatibility\\": "tests/Compatibility/",
37+
"Swh\\SmartRelationSync\\Tests\\Functional\\": "tests/Functional/"
3038
}
3139
},
3240
"config": {

phpstan.neon

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,19 @@ parameters:
2828
consoleApplicationLoader: tests/StaticAnalyze/PHPStan/console-application.php
2929
type_perfect:
3030
no_mixed: true
31+
ignoreErrors:
32+
- identifier: class.extendsInternalClass
33+
- identifier: classConstant.internalClass
34+
- identifier: method.internal
35+
- identifier: method.internalClass
36+
- identifier: method.internalInterface
37+
- identifier: new.internalClass
38+
- identifier: parameter.internalClass
39+
- identifier: parameter.internalInterface
40+
- identifier: property.internalClass
41+
- identifier: property.internalInterface
42+
- identifier: return.internalClass
43+
- identifier: staticMethod.internal
3144

3245
services:
3346
- # register the class, so we can decorate it, but don't tag it as a rule, so only our decorator is used by PHPStan

src/ApiDefinition/EntitySchemaGeneratorDecorator.php

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,9 @@
1111
use Shopware\Core\Framework\Api\Context\AdminApiSource;
1212
use Shopware\Core\Framework\DataAbstractionLayer\EntityDefinition;
1313
use Shopware\Core\Framework\DataAbstractionLayer\Field\Field;
14-
use Shopware\Core\Framework\DataAbstractionLayer\Field\ManyToManyAssociationField;
15-
use Shopware\Core\Framework\DataAbstractionLayer\Field\OneToManyAssociationField;
1614
use Shopware\Core\System\SalesChannel\Entity\SalesChannelDefinitionInterface;
1715
use Swh\SmartRelationSync\DataAbstractionLayer\WriteCommandExtractorDecorator;
16+
use Swh\SmartRelationSync\ValueObject\RelevantField;
1817

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

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

7170
foreach ($relevantFields as $field) {
7271
if (!array_key_exists($field->getPropertyName(), $entityProperties)) {
@@ -88,12 +87,4 @@ public function supports(string $format, string $api): bool
8887
{
8988
return $this->decorated->supports($format, $api);
9089
}
91-
92-
/**
93-
* @phpstan-assert-if-true ManyToManyAssociationField|OneToManyAssociationField $field
94-
*/
95-
private function isRelevantField(Field $field): bool
96-
{
97-
return WriteCommandExtractorDecorator::isRelevantField($field);
98-
}
9990
}

src/ApiDefinition/OpenApiDefinitionSchemaBuilderDecorator.php

Lines changed: 41 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,18 @@
66

77
use OpenApi\Annotations\Property;
88
use OpenApi\Annotations\Schema;
9+
use RuntimeException;
910
use Shopware\Core\Framework\Api\ApiDefinition\DefinitionService;
1011
use Shopware\Core\Framework\Api\ApiDefinition\Generator\OpenApi\OpenApiDefinitionSchemaBuilder;
1112
use Shopware\Core\Framework\Api\Context\AdminApiSource;
1213
use Shopware\Core\Framework\Api\Context\SalesChannelApiSource;
1314
use Shopware\Core\Framework\DataAbstractionLayer\EntityDefinition;
1415
use Shopware\Core\Framework\DataAbstractionLayer\Field\Field;
1516
use Shopware\Core\Framework\DataAbstractionLayer\Field\Flag\ApiAware;
17+
use Shopware\Core\Framework\DataAbstractionLayer\Field\Flag\Extension;
1618
use Shopware\Core\Framework\DataAbstractionLayer\Field\Flag\IgnoreInOpenapiSchema;
17-
use Shopware\Core\Framework\DataAbstractionLayer\Field\ManyToManyAssociationField;
18-
use Shopware\Core\Framework\DataAbstractionLayer\Field\OneToManyAssociationField;
1919
use Swh\SmartRelationSync\DataAbstractionLayer\WriteCommandExtractorDecorator;
20+
use Swh\SmartRelationSync\ValueObject\RelevantField;
2021
use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter;
2122

2223
class OpenApiDefinitionSchemaBuilderDecorator extends OpenApiDefinitionSchemaBuilder
@@ -53,7 +54,7 @@ public function getSchemaByDefinition(
5354
}
5455

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

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

71-
foreach ($relevantSchemas as $schema) {
72-
$schema->properties[] = $property;
73-
}
72+
match ($field->is(Extension::class)) {
73+
true => $this->addExtensionToSchemas($relevantSchemas, $property),
74+
false => $this->addPropertyToSchemas($relevantSchemas, $property),
75+
};
7476
}
7577

7678
return $schemas;
7779
}
7880

81+
/**
82+
* @param Schema[] $schemas
83+
*/
84+
private function addExtensionToSchemas(array $schemas, Property $property): void
85+
{
86+
foreach ($schemas as $schema) {
87+
$extensionSchema = $this->getExtensionSchema($schema);
88+
89+
$extensionSchema->properties[] = $property;
90+
}
91+
}
92+
93+
/**
94+
* @param Schema[] $schemas
95+
*/
96+
private function addPropertyToSchemas(array $schemas, Property $property): void
97+
{
98+
foreach ($schemas as $schema) {
99+
$schema->properties[] = $property;
100+
}
101+
}
102+
103+
private function getExtensionSchema(Schema $schema): Schema
104+
{
105+
foreach ($schema->properties as $property) {
106+
if ($property->property === 'extensions') {
107+
return $property;
108+
}
109+
}
110+
111+
throw new RuntimeException('extensions property not found');
112+
}
113+
79114
/**
80115
* @param Schema[] $schemas
81116
*
@@ -108,14 +143,6 @@ private function getRelevantSchemas(array $schemas, EntityDefinition $definition
108143
return $relevantSchemas;
109144
}
110145

111-
/**
112-
* @phpstan-assert-if-true ManyToManyAssociationField|OneToManyAssociationField $field
113-
*/
114-
private function isRelevantField(Field $field): bool
115-
{
116-
return WriteCommandExtractorDecorator::isRelevantField($field);
117-
}
118-
119146
private function shouldFieldBeIncluded(Field $field, bool $forSalesChannel): bool
120147
{
121148
if ($field->getPropertyName() === 'translations'

src/DataAbstractionLayer/WriteCommandExtractorDecorator.php

Lines changed: 69 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use Shopware\Core\Framework\DataAbstractionLayer\EntityDefinition;
1010
use Shopware\Core\Framework\DataAbstractionLayer\Field\Field;
1111
use Shopware\Core\Framework\DataAbstractionLayer\Field\FkField;
12+
use Shopware\Core\Framework\DataAbstractionLayer\Field\Flag\Extension;
1213
use Shopware\Core\Framework\DataAbstractionLayer\Field\ManyToManyAssociationField;
1314
use Shopware\Core\Framework\DataAbstractionLayer\Field\ManyToOneAssociationField;
1415
use Shopware\Core\Framework\DataAbstractionLayer\Field\OneToManyAssociationField;
@@ -17,6 +18,7 @@
1718
use Shopware\Core\Framework\DataAbstractionLayer\Version\VersionDefinition;
1819
use Shopware\Core\Framework\DataAbstractionLayer\Write\WriteCommandExtractor;
1920
use Shopware\Core\Framework\DataAbstractionLayer\Write\WriteParameterBag;
21+
use Swh\SmartRelationSync\ValueObject\RelevantField;
2022

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

34-
/**
35-
* @phpstan-assert-if-true ManyToManyAssociationField|OneToManyAssociationField $field
36-
*/
37-
public static function isRelevantField(Field $field): bool
38-
{
39-
return $field instanceof ManyToManyAssociationField
40-
|| $field instanceof OneToManyAssociationField;
41-
}
42-
4336
/**
4437
* @param array<mixed, mixed> $rawData
4538
*/
@@ -226,6 +219,65 @@ private function getPrimaryKeyFields(EntityDefinition $reference): array
226219
return $fields;
227220
}
228221

222+
/**
223+
* @param array<mixed, mixed> $rawData
224+
*
225+
* @return array<mixed, mixed>
226+
*/
227+
private function getRelevantRawData(Field $field, array $rawData): array
228+
{
229+
if (!$field->is(Extension::class)) {
230+
return $rawData;
231+
}
232+
233+
$propertyName = $field->getPropertyName();
234+
235+
if (isset($rawData[$propertyName])) {
236+
return $rawData;
237+
}
238+
239+
if (
240+
!isset($rawData['extensions'])
241+
|| !is_array($rawData['extensions'])
242+
|| !isset($rawData['extensions'][$propertyName])) {
243+
return $rawData;
244+
}
245+
246+
return $rawData['extensions'];
247+
}
248+
249+
/**
250+
* @param array<mixed, mixed> $rawData
251+
*/
252+
private function registerFieldForCleanup(
253+
RelevantField $relevantField,
254+
array $rawData,
255+
): void {
256+
$field = $relevantField->field;
257+
258+
$fieldData = $rawData[$field->getPropertyName()] ?? null;
259+
260+
if (!is_array($fieldData)) {
261+
return;
262+
}
263+
264+
$cleanupEnableField = $this->getCleanupEnableFieldName($field);
265+
266+
if (!array_key_exists($cleanupEnableField, $rawData)) {
267+
return;
268+
}
269+
270+
$cleanupEnabled = is_bool($rawData[$cleanupEnableField]) && $rawData[$cleanupEnableField];
271+
272+
unset($rawData[$cleanupEnableField]);
273+
274+
if (!$cleanupEnabled) {
275+
return;
276+
}
277+
278+
$this->registerRelationsForField($relevantField, $fieldData);
279+
}
280+
229281
/**
230282
* @param array<mixed, mixed> $rawData
231283
*/
@@ -247,43 +299,28 @@ private function registerRelations(array $rawData, WriteParameterBag $parameters
247299
$primaryKeys = $this->filterVersionFields($primaryKeys, $definition);
248300

249301
foreach ($definition->getFields() as $field) {
250-
if (!self::isRelevantField($field)) {
251-
continue;
252-
}
253-
254-
$cleanupEnableField = $this->getCleanupEnableFieldName($field);
302+
$relevantField = RelevantField::create($field, $primaryKeys);
255303

256-
if (!array_key_exists($cleanupEnableField, $rawData)) {
304+
if ($relevantField === null) {
257305
continue;
258306
}
259307

260-
$cleanupEnabled = is_bool($rawData[$cleanupEnableField]) && $rawData[$cleanupEnableField];
261-
262-
unset($rawData[$cleanupEnableField]);
263-
264-
if (!$cleanupEnabled) {
265-
continue; // @codeCoverageIgnore
266-
}
267-
268-
$fieldData = $rawData[$field->getPropertyName()] ?? null;
308+
$rawData = $this->getRelevantRawData($field, $rawData);
269309

270-
if (!is_array($fieldData)) {
271-
continue;
272-
}
273-
274-
$this->registerRelationsForField($field, $primaryKeys, $fieldData);
310+
$this->registerFieldForCleanup($relevantField, $rawData);
275311
}
276312
}
277313

278314
/**
279-
* @param non-empty-array<non-empty-string, non-empty-string> $parentPrimaryKey
280315
* @param array<mixed, mixed> $fieldData
281316
*/
282317
private function registerRelationsForField(
283-
ManyToManyAssociationField|OneToManyAssociationField $field,
284-
array $parentPrimaryKey,
318+
RelevantField $relevantField,
285319
array $fieldData,
286320
): void {
321+
$field = $relevantField->field;
322+
$parentPrimaryKey = $relevantField->parentPrimaryKey;
323+
287324
$reference = $field->getReferenceDefinition();
288325

289326
$cleanupRelationData = match ($field instanceof OneToManyAssociationField) {

0 commit comments

Comments
 (0)