Skip to content

Commit dd90dc2

Browse files
committed
Fix ComponentGroup.allowedComponents: add Layout:read/Page:read groups and FQCN normalisation
Primary fix: add Layout:read and Page:read to allowedComponents Groups so the field is present when a ComponentGroup is serialised in Layout or Page context, preventing the Nuxt ComponentGroupUtilSynchronizer from firing spurious PATCHes that blank the nav. Secondary fix: ComponentGroupNormalizer now implements DenormalizerInterface. Any PHP FQCN sent in allowedComponents (e.g. from a misconfigured client) is converted to a collection IRI before being stored, matching the format ComponentGroupNormalizer reads on the normalization side. Behat: new scenario verifies FQCN is converted to IRI in the PATCH response.
1 parent 5517ade commit dd90dc2

3 files changed

Lines changed: 51 additions & 2 deletions

File tree

features/main/component_groups.feature

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,3 +125,14 @@ Feature: ComponentGroup resource
125125
| json_decode([ "resource[layout]" ]) |
126126
Then the response status code should be 200
127127
And the resource "layout" should be purged from the cache
128+
129+
@loginAdmin
130+
Scenario: Sending a PHP class name as allowedComponents is normalised to a collection IRI
131+
Given there is a ComponentGroup with 0 components
132+
When I send a "PATCH" request to the resource "component_group" with body:
133+
"""
134+
{"allowedComponents": ["Silverback\\ApiComponentsBundle\\Tests\\Functional\\TestBundle\\Entity\\DummyComponent"]}
135+
"""
136+
Then the response status code should be 200
137+
And the JSON node "allowedComponents[0]" should be equal to "/component/dummy_components"
138+

src/Entity/Core/ComponentGroup.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ class ComponentGroup
7474
public Collection $componentPositions;
7575

7676
#[ORM\Column(name: 'allowed_components', type: 'json', nullable: true)]
77-
#[Groups(['ComponentGroup:read', 'ComponentGroup:write'])]
77+
#[Groups(['ComponentGroup:read', 'ComponentGroup:write', 'Layout:read', 'Page:read'])]
7878
public ?array $allowedComponents = null;
7979

8080
public function __construct()

src/Serializer/Normalizer/ComponentGroupNormalizer.php

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@
1616
use ApiPlatform\Metadata\UrlGeneratorInterface;
1717
use Silverback\ApiComponentsBundle\Entity\Core\ComponentGroup;
1818
use Silverback\ApiComponentsBundle\Entity\Core\ComponentPosition;
19+
use Symfony\Component\Serializer\Normalizer\DenormalizerAwareInterface;
20+
use Symfony\Component\Serializer\Normalizer\DenormalizerAwareTrait;
21+
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
1922
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
2023
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;
2124
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
@@ -25,11 +28,15 @@
2528
* pageDataProperty positions (no component) are always kept.
2629
* Components that violate the restriction remain in the database — they are only hidden from the response.
2730
*
31+
* On write, converts any PHP FQCN in allowedComponents to a collection IRI so the stored value is
32+
* always an IRI (e.g. /component/navigation_links) regardless of what the client sends.
33+
*
2834
* @author Daniel West <daniel@silverback.is>
2935
*/
30-
class ComponentGroupNormalizer implements NormalizerInterface, NormalizerAwareInterface
36+
class ComponentGroupNormalizer implements NormalizerInterface, NormalizerAwareInterface, DenormalizerInterface, DenormalizerAwareInterface
3137
{
3238
use NormalizerAwareTrait;
39+
use DenormalizerAwareTrait;
3340

3441
private const ALREADY_CALLED = 'COMPONENT_GROUP_NORMALIZER_ALREADY_CALLED';
3542

@@ -74,6 +81,37 @@ function (ComponentPosition $position) use ($allowed): bool {
7481
return $result;
7582
}
7683

84+
public function supportsDenormalization($data, $type, $format = null, array $context = []): bool
85+
{
86+
return ComponentGroup::class === $type && !isset($context[self::ALREADY_CALLED]);
87+
}
88+
89+
public function denormalize($data, $type, $format = null, array $context = []): ComponentGroup
90+
{
91+
$context[self::ALREADY_CALLED] = true;
92+
93+
/** @var ComponentGroup $object */
94+
$object = $this->denormalizer->denormalize($data, $type, $format, $context);
95+
96+
if (null !== $object->allowedComponents) {
97+
$object->allowedComponents = array_values(array_map(
98+
function (string $value): string {
99+
if (!str_contains($value, '\\')) {
100+
return $value;
101+
}
102+
return $this->iriConverter->getIriFromResource(
103+
$value,
104+
UrlGeneratorInterface::ABS_PATH,
105+
(new GetCollection())->withClass($value),
106+
);
107+
},
108+
$object->allowedComponents,
109+
));
110+
}
111+
112+
return $object;
113+
}
114+
77115
public function getSupportedTypes(?string $format): array
78116
{
79117
return [ComponentGroup::class => false];

0 commit comments

Comments
 (0)