symfony / ux

Symfony UX initiative: a JavaScript ecosystem for Symfony

Home Page:https://ux.symfony.com/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

[TwigComponent] PreMount VS required props

cavasinf opened this issue · comments

First time using TwigComponent, but works like a charm good job 👍
I've stumbled upon a case that I can't accomplish without feeling like "it's too much code".

Simple case:

  • Required props title.
  • Pass Entity to the props which is Stringable.
  • Component used like this:
    <twig:DropdownHeaderItem title="{{ app.user }}"/>

Here's the Component:

// src/Twig/Components/DropdownHeaderItem.php

#[AsTwigComponent]
final class DropdownHeaderItem extends DropdownItem
{
    public string $title;
}

And his twig:

{# templates/components/DropdownHeaderItem.html.twig #}

<h6
        {{ attributes }}
        class="dropdown-header {{ this.extraClass }}" {{ macros.attr_to_html(this.attr) }}
>
    {{ this.title }}
</h6>

Sadly, this is not enough to make the title option required AND with an explicit error message.
If you call the component with no parameters:

<twig:DropdownHeaderItem/>

This is the error that is displayed:
image

Two problems:

  1. It show the error INSIDE his component template and not where it is used.
  2. The stack trace (as usual with twig) doesn't show where the error occured in twig (only controller with ->render()...)

Looking at the doc, we should use PreMount hook with the OptionsResolver like this:

#[AsTwigComponent]
final class DropdownHeaderItem extends DropdownItem
{
    public string $title;

    #[PreMount]
    public function preMount(array $data): array
    {
        $resolver = new OptionsResolver();
        $resolver->setRequired('title');
        $resolver->setAllowedTypes('title', ['string', \Stringable::class]);

        return $resolver->resolve($data);
    }
}

I'm not a fan of that because:

  1. Nowadays we can do a lot with only PHP8 (declarations/types/attributes) for simple cases.
    PHP auto __toString() call if property typed to string when class implements the method.
  2. The string reference 'title' to the property $title (maintainability).
  3. You need to declare ALL other properties, otherwise you'll get The option "xxxx" does not exist
  4. All the default value from the PHP declaration, need to be redefined IN the OptionsResolver.
  5. +9 lines of code just for a required.

Is there any limitation, to not be able to print required error without the OptionResolver?

Your property is typed string, so even if you allow stringable in the option, it wont be set during mount without some code i think.

What surprises me is that the error is not thrown earlier.

What happens if :

  • you give explicitly a string to the component
  • you cast your title during mount
public function mount(string|\Stringable $title): void
{
      $this->title = (string) $title;
}

What surprises me is that the error is not thrown earlier.

All of the code above (from my example), doesn't trigger any error though.

An error is thrown if the title argument is missing,
but with a message that is not explicit at all if there is no OptionsResolver

So you suggest to improve the error message, right ? Or is the "stringable" thing more your point there ?
i'm asking to be certain to understand the main issue :)

What would do you suggest the message 'say' ?

Yes, it's more of a question/improvement than a bug (I should have been more/less specific, sorry).

The main question is:
Is there any limitation to not be able to define component usage without the OptionResolver (preMount)?

Can't we define the component configuration from the component property itself?
Like entity bound to Symfony Form:

  • Required (nullable or not)
  • Type
  • Default value
  • Allowed values (perhaps from an enum)
  • Visible/Settable (public or private + Symfony\UX\TwigComponent\Attribute\ExposeInTemplate)

For example, what the OptionsResolver tells "more" than the property definition:

#[AsTwigComponent]
final class Alert
{
    public string $message;

    #[PreMount]
    public function preMount(array $data): array
    {
        $resolver = new OptionsResolver();
        $resolver->setIgnoreUndefined();

        $resolver->setRequired('message');
        $resolver->setAllowedTypes('message', 'string');

        return $resolver->resolve($data);
    }
}
  • Required: because string not nullable and NO default value

  • Type: string as defined by the type

  • Default value: as defined directly in the PHP property

     #[AsTwigComponent]
     final class Alert
     {
         public string $type = 'success';
     
         #[PreMount]
         public function preMount(array $data): array
         {
             $resolver = new OptionsResolver();
             $resolver->setIgnoreUndefined();
     
             $resolver->setDefaults(['type' => 'success']);
     
             return $resolver->resolve($data);
         }
     }
  • Allowed values can be defined through an Enum:

    enum ColorEnum: string
    {
        case Primary        = 'primary';
        case Secondary      = 'secondary';
    
        case Success      = 'success';
        case Warning      = 'warning';
        case Danger       = 'danger';
        case Info         = 'info';
    }
    #[AsTwigComponent]
    final class Alert
    {
        public ColorEnum $color;
    
        #[PreMount]
        public function preMount(array $data): array
        {
            $resolver = new OptionsResolver();
            $resolver->setIgnoreUndefined();
    
            $resolver->setRequired('color');
            $resolver->setAllowedValues('color', [
                'primary',
                'secondary',
                'success',
                'warning',
                'danger',
            ]);
    
            return $resolver->resolve($data);
        }
    }

LiveComponent uses validator and constraints ... could we find a way to use it there ?

What make me doubts regarding the OptionsResolver is that it will not work with LiveComponent and i would like to avoid users needing to have two parralel validation systems ..

I agree here! With an anonymous component when you set a variable without a default variable, you get an error telling you that this prop is required to use this component. But with a standard class component, you can't make your props mandatory just by not setting a default variable. I think it could be a nice improvement but this is a huge BC break! What we can do in a PR is add an option strictPropsif this option is set to true we throw an error if a component is used without giving a props with no default value. WDYT?

What if..... we introduce a "Prop" argument ... that could be some "LiveProp" but for TwigComponent ?

That would allow us to add type, constraints, hydration, ...

Do we really need this argument? What about public props but not with the Prop argument? What do you mean by type?

Just thinking out loud / suggesting something to handle the BC :)