'Failing validation doesn't stop code execution in livewire component

I'm trying to show a list of models. Having a text filter posed no problem but when trying to add a year filter, problems started occurring.

If a non-numeric value is sent back from the from or to input, I expected validation to catch it and just flash some errors to the session but instead I still get SQL errors because either from or to were not numeric (for example: invalid input syntax for integer: "2020a" (SQL: select * f rom "my_models" where "year" between 2020a and 2021))

Livewire Component code

class ModelList extends Component
{
    use WithPagination;
    
    public $from;
    public $to;
    public $search;

    protected $models;

    protected $rules = [
        'from'   => ['required', 'integer', 'between:1970,2021'],
        'to'     => ['required', 'integer', 'between:1970,2021'],
        'search' => ['nullable', 'string'],
    ];

    public function mount()
    {
        $this->from   = (int) date('Y') - 10;
        $this->to     = (int) date('Y');
        $this->search = null;

        $this->doQuery();
    }

    public function updating($name, $value)
    {
        $this->validate();
        $this->doQuery();
        $this->resetPage();
    }

    public function render()
    {
        return view('livewire.list', ['models' => $this->models]);
    }

    protected function doQuery()
    {
        $this->models = MyModel::query()
            ->whereBetween('year', [$this->from, $this->to])
            ->search($this->search)
            ->orderByDesc('year')
            ->paginate(10);
    }
}

View

<div>
    <div id="filters" class="flex flex-col text-gray-800">
        <div>
            <input type="number" wire:model.lazy="from" min="1970" max="{{ date('Y') }}">
            @error('from')
                <span class="text-red-800">{{ $message }}</span>
            @enderror
        </div>
        <div>
            <input type="number" wire:model.lazy="to" min="1970" max="{{ date('Y') }}">
            @error('to')
                <span class="text-red-800">{{ $message }}</span>
            @enderror
        </div>
        <div>
            <input type="text" wire:model.lazy="search">
            @error('search')
                <span class="text-red-800">{{ $message }}</span>
            @enderror
        </div>
    </div>
    <div id="list">
        @foreach ($models as $model)
            {{ $model->id }}<br>
        @endforeach
    </div>
    <div id="pagination">
        {{ $models->links() }}
    </div>
</div>

I've tried different things like

  • just doing the query inside the render() method, inlining the doQuery() method.
  • just doing the query inside updating() method.
  • just doing the query inside mount() method.
  • doing the query inside both updating(), mount(), and render() methods.
  • calling $this->validate() inside render()

but none of the above seem to work with bad input. Sometimes $models gets unset and I get an error for calling the links() method on it. If that doesn't happen, then I get an SQL error because the bad input went right through the validation.

I know I could use a select input or add some javascript to prevent non-numeric values but that doesn't fix the underlying issue of not being able to trust Livewire to validate my input. (And it only takes opening the console and writing a couple of lines to completely invalidate such flimsy validation.)



Solution 1:[1]

If you really want to validate your input you have to wrap your input fields into a form tag. like:

<form>
    <div id="filters" class="flex flex-col text-gray-800">
        <div>
            <input type="number" wire:model.lazy="from" min="1970" max="{{ date('Y') }}">
            @error('from')
                <span class="text-red-800">{{ $message }}</span>
            @enderror
        </div>
        <div>
            <input type="number" wire:model.lazy="to" min="1970" max="{{ date('Y') }}">
            @error('to')
                <span class="text-red-800">{{ $message }}</span>
            @enderror
        </div>
        <div>
            <input type="text" wire:model.lazy="search">
            @error('search')
                <span class="text-red-800">{{ $message }}</span>
            @enderror
        </div>
    </div>
</form>

Solution 2:[2]

This was giving me a hard time too. The problem is that in Livewire and Laravel, if $this->validate() fails, it throws an Exception. In Livewire this will trigger the front end display of error messages for the inputs as expected. But the Livewire render() method still triggers. It triggers every time properties change on the component.

The solution I used, for a customer search component, is to check the ErrorBag on each render call, and if there are any messages, set a property that I use to determine what to actually render. In my case, if there are validation errors, I pass an empty Collection for $customers, and then check if $customers->count() > 0 in the blade template.

In CustomerSearch component:

 public function render()
    {
        $this->isValidInput = $this->getErrorBag()->count() == 0;

        if ($this->isValidInput == false)
        {
            return view('customers.customer-search', ['customers' => Collection::empty()]);
        }
        else
        {
            return view('customers.customer-search',
                ['customers' => Customer::search($this->params)->paginate(10)]);
        }
    }

In customer-search.blade:

<div>
    <table>
        <tr>
            <th>Customer ID</th>
            <th>First Name</th>
            <th>Last Name</th>
            <th>Email</th>                                            
        </tr>

        @if ($customers->count() == 0)

            <tr>
                <td colspan="4" class="text-center">No matching results.</td>
            </tr>

        @else

            @foreach($customers as $customer)
                <tr>
                    <td>{!! $customer->CustID !!}</td>
                    <td>{!! $customer->NameFirst !!}</td>
                    <td>{!! $customer->NameLast !!}</td>
                    <td>{!! $customer->Email !!}</td>                    
                </tr>
            @endforeach

        @endif

    </table>

    @if ($customers->count() > 0)
        {{ $customers->links() }}
    @endif
        
</div>

    
    

Hope that helps!

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Azahar Alam
Solution 2 Michael A