symfony / ux

Symfony UX initiative: a JavaScript ecosystem for Symfony
https://ux.symfony.com/
MIT License
820 stars 297 forks source link

[LiveComponent] Passing an entire object (not an entity) to listeners #1483

Closed gremo closed 1 month ago

gremo commented 7 months ago

I've a Notifier component which is listening to the "show" event and accept a Notification object:

#[LiveListener('show')]
public function show(#[LiveArg] Notification $notification)
{
    $this->notification = $notification;
}
$this->emit('show', ['notification' => new Notification()], 'Notifier');

Right now, when I try to emit the event, an error occurred:

Could not resolve argument $notification of "App\Twig\Components\Notifier::show()", maybe you forgot to register the controller as a service or missed tagging it with the "controller.service_arguments"?

... because argoment is "translated" (serialized I assume) as an array.

In the docs the example works because Product is an entity, and the param converter system kicks in, transforming the int passed to emit back to an object.

How to obtain the same result in my example?

smnandre commented 7 months ago

As stated in the documentation, emit() only accept some scalars in the array, so you won't be able to pass "new Notification()" like that (at least not a specific instance)

You can also pass extra (scalar) data to the listeners

So i see two potential options (there may be more):

Last "option" / "alternative": if you dispatch this Event from PHP, and only listen to it in another PHP class... you could also use a standard EventDispatcher and avoid a maybe-unwanted roundtrip via the front-end (not sure if applicable in your case, but sometimes it can be a good idea)

gremo commented 7 months ago

@smnandre thanks for the idea. Do the hydrateWith / dehydrateWith process occurs also when using #[LiveArg]? For what I understand from the docs, it seems that the process is used only when passing props to the component....

smnandre commented 7 months ago

There may be a better way, but i tried something and i believe it suits your needs :

Twig Component


#[AsLiveComponent('Foo')]
class Foo extends AbstractController
{
    use DefaultActionTrait;
    use ComponentToolsTrait;

    #[LiveAction]
    public function bar(): void
    {
        $this->emit('FooBar', ['notification' => new Notification('Hello', 'World!')]);
    }

    #[LiveListener('FooBar')]
    public function onFooBar(
        #[LiveArg('notification')] #[ValueResolver(NotificationResolver::class)] Notification $notification
    )
    {
        dump($notification);
    }
}

Twig Template

<div {{ attributes.defaults({}) }}>

    <button
        type="button"
        data-action="live#action"
        data-action-name="bar()"
    >BAR</button>

</div>

Notification DTO

class Notification
{
    public function __construct(
        public string $title,
        public string $message,
    ) {
    }

    public function getTitle(): string
    {
        return $this->title;
    }

    public function getMessage(): string
    {
        return $this->message;
    }
}

And a little ValueResolver


class NotificationResolver implements ValueResolverInterface
{
    public function resolve(Request $request, ArgumentMetadata $argument): array
    {
        if (Notification::class !== $argument->getType()) {
            return [];
        }

        if (!is_array($attribute = $request->attributes->get($argument->getName()))) {
            return [];
        }

        if (!isset($attribute['title']) || !isset($attribute['message'])) {
            return [];
        }

        if (!is_string($attribute['title']) || !is_string($attribute['message'])) {
            throw new NotFoundHttpException(sprintf('The notification for the "%s" parameter is invalid.', $argument->getName()));
        }

        return [new Notification($attribute['title'], $attribute['message'])];
    }
}

And when you click on the button, the live action is called, the event is emitted, and you can get the notification object as you wanted.

Requests Debug
Capture d’écran 2024-02-10 à 16 52 01 Capture d’écran 2024-02-10 à 16 52 10
carsonbot commented 1 month ago

Thank you for this issue. There has not been a lot of activity here for a while. Has this been resolved?