Skip to content

Pattern for CQRS commands taking typed VO arrays (blocks BusinessEntity, likely more) #413

Description

@PrestaEdit

While working on the endpoints tracked by PrestaShop/PrestaShop#39630, I hit a case that the current `ps_apiresources` conventions don't cover:

`AddBusinessEntityCommand` (on PrestaShop `develop`) has this shape:

```php
public function __construct(
// ...scalars + enum...
/** @PARAM array $billingAddresses /
private readonly array $billingAddresses = [],
/
* @PARAM array $shippingAddresses */
private readonly array $shippingAddresses = [],
)
```

The constructor calls VO methods on the array items (`$address->isDefault()`), so the CQRS bus expects them already hydrated as `BusinessEntityBillingAddress` / `BusinessEntityShippingAddress` instances.

What I tried

A plain `CQRSCreate` operation with a resource DTO exposing `public array $billingAddresses;` and the request body carrying associative arrays for each address (`{alias, address1, address2, city, postcode, countryId, stateId, phone, phoneMobile, default}`). Full draft: PrestaEdit#3.

What happens

```
500 - Call to a member function isDefault() on array
at AddBusinessEntityCommand.php:153
```

`CQRSApiSerializer` doesn't hydrate the array items into VOs — Symfony's Serializer, as wired, does not read the `@param array` docblock on the target command constructor to type-cast collection elements. The addresses reach the command as raw associative arrays and the ctor blows up.

Why I can't fix it under current rules

`CONTEXT.md` forbids:

  • custom normalizers,
  • custom processors,
  • Value Objects as resource properties.

`CQRSCommandMapping` (`[apiField] => [commandParam]`) only routes keys; it cannot instantiate nested types.

Question

What's the intended module-side pattern when a CQRS command param is a typed array of VOs?

Possibilities I can see:

  1. A blessed exception: allow per-resource `Denormalizer` implementations under a specific namespace, listed as an approved escape hatch in `CONTEXT.md`.
  2. A framework enhancement: teach `CQRSApiSerializer` (or a decorator) to read `@param array` docblocks on the target command's constructor and hydrate items via reflection — no per-resource code needed.
  3. A convention on core: recommend a scalar-friendly companion command (e.g. `AddBusinessEntityFromScalarsCommand`) that accepts `array` and constructs the VOs internally, letting the module stay simple.
  4. A dedicated `CQRSCreate` option: something like `nestedTypes: ['[billingAddresses]' => BusinessEntityBillingAddress::class]` that the processor honors.

Happy to prototype whichever direction the maintainers want. This isn't just `BusinessEntity` — it will hit any future endpoint whose write command takes VO collections.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    Status
    Waiting for QA

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions