Nitro

Essentials

Arrays and models

Most state is arrays or Eloquent models. You read an array with square brackets and a model with an arrow, exactly as in PHP, and both work the same in the browser and on the server.

On this page

A view runs twice: as Blade on the server, and compiled in the browser. So the rules are PHP's rules. An array is read with $task['title']. A model, or any other object, is read with $post->title. Nitro keeps track of which values are models, so the browser reads them just as Laravel does.

Reading arrays and models

Use the syntax that fits the value:

Blade
{{-- An array: square brackets --}}
<p>{{ $task['title'] }}</p>
<p>{{ $task['owner']['name'] ?? 'Nobody' }}</p>

{{-- A model: an arrow, and its loaded relations the same way --}}
<p>{{ $post->title }}</p>
<p>{{ $post->author->name }}</p>
<p>{{ $post->editor?->name ?? 'Not edited' }}</p>
  • Arrays use square brackets. Nested keys work the same way, as deep as you need.
  • Models use an arrow, and so do their loaded relations. $post['title'] also works on a model, as it does in Laravel.
  • ?-> and ?? handle a missing relation or key, as they do in PHP.

An arrow on an array fails

$task->title on an array fails with Attempt to read property "title" on array. It fails in the browser just as it fails in a Laravel view, so you see the mistake at once, not on some later render. Use $task['title'] instead.

Working with arrays

Arrays suit data that the visitor changes in the page: form rows, a cart, a draft, a list you filter. Your browser methods change them with the usual PHP, without a request:

PHP
#[Validate([
    'lines' => 'required|array|max:50',
    'lines.*.description' => 'required|max:200',
    'lines.*.quantity' => 'integer|min:1|max:999',
    'lines.*.price' => 'integer|min:0',          // In pence, typed in by the user.
])]
public array $lines = [
    ['description' => '', 'quantity' => 1, 'price' => 0],
];

public function addLine(): void
{
    $this->lines[] = ['description' => '', 'quantity' => 1, 'price' => 0];
}

public function removeLine(int $i): void
{
    array_splice($this->lines, $i, 1);
}

#[Computed]
public function total(): int
{
    return array_sum(array_map(fn (array $line) => $line['quantity'] * $line['price'], $this->lines));
}

#[Server]
public function save(): void
{
    $this->validate();

    auth()->user()->invoices()->create([
        'lines' => $this->lines,
        'total' => $this->total,      // Worked out again here, from the validated lines.
    ]);
}
Blade
@foreach ($lines as $i => $line)
    <div>
        <input nitro:model="lines.{{ $i }}.description">
        <input type="number" nitro:model.number="lines.{{ $i }}.quantity">
        <span>{{ number_format($line['quantity'] * $line['price'] / 100, 2) }}</span>
        <button nitro:click="removeLine($i)">Remove</button>
    </div>
@endforeach

<button nitro:click="addLine">Add a line</button>
<p>Total: {{ number_format($this->total / 100, 2) }}</p>
  • Write to any depth. $this->lines[$i]['quantity']++ and $this->lines[] = [...] work in browser methods, as in PHP.
  • Bind any depth. nitro:model="lines.{{ $i }}.quantity" binds an input to one row's field, on an input or on a child component's tag.
  • Use PHP's array functions. array_map(), array_filter(), usort(), array_splice() and the rest run in the browser.
  • Arrays are values. $copy = $this->lines makes a copy, so changing $copy never changes the property.
  • Keys and their order survive. An array keyed by id stays keyed by id, in its order, on the way to the browser and back.

Validate what the browser changed

The browser can set an array to anything. Give it #[Validate] rules with * for each row, and call $this->validate() before you save. Work out totals again on the server: the browser's total is a preview.

Working with models

Models suit records that live in the database, which your #[Server] methods act on. Load them in mount(), with the relations the view needs:

PHP Server
use App\Models\Post;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Facades\Gate;

public Collection $posts;

public function mount(): void
{
    $this->posts = Post::with('author')->latest()->take(20)->get();
}

#[Server]
public function publish(Post $post): void
{
    Gate::authorize('update', $post);
    $post->update(['published_at' => now()]);
}
Blade
@foreach ($posts as $post)
    <article nitro:key="{{ $post->id }}">
        <h3>{{ $post->title }}</h3>
        <p>By {{ $post->author->name }}</p>

        @if ($post->published_at === null)
            <button nitro:click="publish($post)">Publish</button>
        @endif
    </article>
@endforeach
  • The browser gets their visible attributes. Hidden attributes ($hidden) are never sent.
  • Load relations with with(). The browser can't query the database, so a relation you didn't load isn't there. Nitro loads the same relations again on every #[Server] call.
  • The server reads them again on every call. A model property always comes from the database, never from what the browser sends back.
  • Change them on the server. Update a record in a #[Server] method. The next state the browser gets has the new values.

Passing them to server methods

Pass an array's id, or the model itself. A parameter typed as a model receives the record:

In the view The method Receives
remove($task['id']) remove(int $id) The id. Look the record up yourself
remove($post->id) remove(Post $post) The record the key names
remove($post) remove(Post $post) The same record: the browser sends its key

The key comes from the browser, so authorize the record before you act on it. See Passing arguments.

Choosing between them

Pick by who owns the data:

When Use For example
The visitor changes it in the page An array Form rows, a cart, a draft, a filter
It lives in the database and the server acts on it A model or a collection Posts to publish, orders to ship
A long list the view only shows An array of the fields it shows Smaller state, and nothing extra sent

Turning models into arrays takes one call:

PHP Server
// Models: read again from the database on every #[Server] call.
$this->posts = Post::with('author')->latest()->get();

// Arrays: only the fields the view needs, and the browser may change them.
$this->posts = Post::latest()->get(['id', 'title'])->toArray();

Next steps