Dans cet article je vous propose de découvrir Livewire qui est un outil, pour Laravel, qui permet de créer des éléments d'interface dynamique directement depuis PHP (sans utiliser de JavaScript).
Principe de base
Dans un premier temps on va créer une classe qui va représenter notre composant et son état.
php artisan make:livewire UsersTable
Cela va permettre de créer une vue blade et une classe PHP qui représentera notre composant. Les propriétés publiques représentent l'état de notre composant. Lorsqu'une de ces propriété change, le code HTML du composant sera regénéré et renvoyé au client.
<?php
namespace App\Livewire;
use App\Models\User;
use Livewire\Component;
class UsersTable extends Component
{
public string $search = "";
public function render()
{
$query = User::orderBy($this->sortField, $this->sortDirection);
if ($this->search) {
$query = $query->whereLike('name', '%' . $this->search . '%');
}
return view('livewire.users-table', [
'users' => $query->paginate(10)
]);
}
}
Ensuite dans la vue, il sera possible d'utiliser des attributs spéciaux, compris par livewire, pour faire évoluer l'état de notre composant.
<div>
<input
type="text"
wire:model.live.debounce.500ms="search"
placeholder="Rechercher un utilisateur"
/>
<table>
<thead>
<tr>
<th>Nom</th>
<th>Rôle</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
@foreach($users as $user)
<tr wire:key="{{ $user->id }}">
<!-- <td> -->
</tr>
@endforeach
</tbody>
</table>
{{ $users->links() }}
</div>
Enfin, le composant livewire peut ensuite être utilisé dans n'importe quelle vue :
@extends('base') @section('body')
<h1>Utilisateurs</h1>
<livewire:users-table></livewire:users-table>
@endsection
Maintenant, dès que l'utilisateur tape dans le champ de recherche, le contenu du tableau se mettra à jour automatiquement sans avoir à rafraîchir la page.
Fonctionnement interne
Même si ça parait magique il est important de comprendre comment Livewire fonctionne pour ne pas faire d'erreur de conception.
- Lorsque le composant est rendu pour la première fois, le code HTML est généré et un attribut spécial
wire:snapshot
est ajouté à l'élément avec une représentation JSON des propriétés publiques. - Lorsque l'on appelle une méthode livewire, ou lors d'une mise à jour via un
wire:model
une requête est envoyée au serveur avec le nom du model ou de la méthode à appeler et le snapshot courant. - Livewire utilise les données du snapshot pour recréer notre composant avec les propriétés remplies pour représenter l'état actuel du composant.
- L'état du composant est changé (à l'aide de la méthode appelée ou via la propriété associé au wire:model).
- La nouvelle structure HTML et le nouveau snapshot sont renvoyés au client.
- Livewire utilise un système de "morphing" pour mettre à jour la structure HTML du client de manière efficace.
Sécurité
En suivant la logique de fonctionnement on peut se dire que Livewire nous expose à des problèmes de sécurité.
Snapshot et checksum
Le premier problème que l'on peut imaginer et l'altération du snapshot. Si un JSON représente l'état de notre composant, qu'est ce qui empèche le client de le modifier de manière arbitraire ?
Pour éviter ce problème, le snapshot est accompagné d'une clef de vérification (checksum) qui est générée côté serveur. Ainsi, si l'utilisateur modifie une propriété du JSON, la clef de vérification ne correspondra plus et le serveur rejettera l'état envoyé.
Sérialisation
L'autre problème est la serialisation des données, comment est transféré un modèle eloquent par exemple ?
Livewire ne permet pas d'exposer n'importe quel type de propriété et seul un objet serialisable peut être utilisé comme propriété publique (il est possible de rendre un objet serialisable en implémentant l'interface Wireable de Livewire). Pour les modèles Eloquent, Livewire les sérialise de la manière suivante :
{
"user": {
"class": "App\\\\Models\\\\User",
"key": 108
}
}
Lorsque Livewire récupère un snapshot avec un modèle, il va donc refaire une requête pour trouver l'élément qui correspond à la clef donnée. Les attributs ne sont pas directement exposés au client. Dans le cas d'une collection, c'est la liste des ids qui sera renvoyée :
{
"users": {
"class": "App\\\\Models\\\\User",
"keys": [1, 2, 3, 4]
}
}
"Never trust a user"
Enfin, le dernier problème, et le plus critique concerne le wire:model
et les méthodes pouvant être appelées depuis le front-end. En effet, rien n'empèche l'utilisateur de modifier le code HTML et de changer le wire:model
d'un champ pour essayer de modifier des propriétés auxquelles il n'aurait normalement pas accès. Il faut donc traiter toutes les actions utilisateur comme potentiellement malveillantes.
Si une propriété n'a pas vocation à être utilisée avec un wire:model
il est possible de la verrouiller avec un simple attribut PHP
<?php
namespace App\Livewire;
use App\Models\User;
use Livewire\Attributes\Locked;
use Livewire\Component;
class UsersTable extends Component
{
public string $search = "";
#[Locked]
public string $sortField = 'name';
#[Url]
public string $sortDirection = 'ASC';
public function sortBy(string $sortField) {
if ($sortField === $this->sortField) {
$this->sortDirection = $this->sortDirection === 'ASC' ? 'DESC' : 'ASC';
} else {
$this->sortDirection = 'ASC';
$this->sortField = $sortField;
}
}
}
Aussi, dans le cas des méthodes, il faut partir du principe que l'utilisateur peut modifier le code HTML pour appeler les méthodes avec des valeurs abitraires. Il est donc nécessaire de valider les paramètres :
private array $sortableFields = ['name', 'active'];
public function sortBy(string $sortField) {
// On évite des champs d'organisation arbitraires
if (!in_array($sortField, $this->sortableFields)) {
return;
}
if ($sortField === $this->sortField) {
$this->sortDirection = $this->sortDirection === 'ASC' ? 'DESC' : 'ASC';
} else {
$this->sortDirection = 'ASC';
$this->sortField = $sortField;
}
}
Dans le cas des formulaires il est possible de rajouter des attributs sur les propriétés pour les valider ensuite
#[Validate('min:4')]
public string $name = '';
#[Validate('email')]
public string $email = '';
public function save()
{
$data = $this->validate();
$this->user->fill($data);
$this->user->save();
}
Pour plus d'informations et de détails, n'hésitez pas à vous rendre sur la documentation officielle