Après les catégories, les articles et les pages, qui constituent l'essentiel du contenu du CMS, venons-en maintenant aux utilisateurs, qui en sont les acteurs et spectateurs, sans eux le CMS n'aurait évidemment aucun sens. On doit pouvoir gérer ces utilisateurs, les lister, modifier au besoin leurs données (nom, email, rôle...). je vais adopter la même démarche que pour les autres éléments du blog.
Pour rappel, la table des matières est ici.
Un composant pour les comptes
On a à nouveau besoin d'un composant Volt pour gérer le tableau des comptes et le code PHP qui va faire tout le traitement :
php artisan make:volt admin/users/index --class
On va ajouter la route pour l'atteindre :
Route::middleware(IsAdmin::class)->group(function () {
...
Volt::route('/users/index', 'admin.users.index')->name('users.index');
});
Et dans la foulée un item dans la barre latérale (admin.sidebar) :
@if (Auth::user()->isAdmin())
...
<x-menu-item icon="s-user" title="{{ __('Accounts') }}" link="{{ route('users.index') }}" />
@endif
On ajoute une traduction :
"Accounts": "Comptes",
On peut désormais atteindre facilement le nouveau composant, il ne nous reste plus qu'à le compléter.
Le tableau des comptes
Voilà le code complet du composant users.index, je vais commenter après les points essentiels :
<?php
use App\Models\User;
use Illuminate\Pagination\LengthAwarePaginator;
use Livewire\Attributes\{Layout, Title};
use Livewire\Volt\Component;
use Livewire\WithPagination;
use Mary\Traits\Toast;
new #[Title('Users'), Layout('components.layouts.admin')]
class extends Component {
use Toast, WithPagination;
public string $search = '';
public array $sortBy = ['column' => 'name', 'direction' => 'asc'];
public string $role = 'all';
public array $roles = [];
public function deleteUser(User $user): void
{
$user->delete();
$this->success($user->name . ' ' . __('deleted'));
}
// Définir les en-têtes de table.
public function headers(): array
{
$headers = [['key' => 'name', 'label' => __('Name')], ['key' => 'email', 'label' => 'E-mail'], ['key' => 'role', 'label' => __('Role')], ['key' => 'valid', 'label' => __('Valid')]];
if ('user' !== $this->role) {
$headers = array_merge($headers, [['key' => 'posts_count', 'label' => __('Posts')]]);
}
return array_merge($headers, [['key' => 'comments_count', 'label' => __('Comments')], ['key' => 'created_at', 'label' => __('Registration')]]);
}
public function users(): LengthAwarePaginator
{
$query = User::query()
->when($this->search, fn($q) => $q->where('name', 'like', "%{$this->search}%"))
->when($this->role !== 'all', fn($q) => $q->where('role', $this->role))
->withCount('posts', 'comments')
->orderBy(...array_values($this->sortBy));
$users = $query->paginate(10);
$userCountsByRole = User::selectRaw('role, count(*) as total')
->groupBy('role')
->pluck('total', 'role');
$totalUsers = $userCountsByRole->sum();
$this->roles = collect([
'all' => __('All') . " ({$totalUsers})",
'admin' => __('Administrators'),
'redac' => __('Redactors'),
'user' => __('Users'),
])->map(function ($roleName, $roleId) use ($userCountsByRole) {
$count = $userCountsByRole->get($roleId, 0);
return [
'name' => $roleId === 'all' ? $roleName : "{$roleName} ({$count})",
'id' => $roleId
];
})->values()->all();
return $users;
}
public function with(): array
{
return [
'users' => $this->users(),
'headers' => $this->headers(),
];
}
}; ?>
<div>
<x-header separator progress-indicator>
<x-slot:title>
<a href="/admin/dashboard" title="{{ __('Back to Dashboard') }}">
{{ __('Users') }}
</a>
</x-slot:title>
<x-slot:middle class="!justify-end">
<x-input placeholder="{{ __('Search') }}..." wire:model.live.debounce="search" clearable
icon="o-magnifying-glass" />
</x-slot:middle>
</x-header>
<x-radio inline :options="$roles" wire:model="role" wire:change="$refresh" />
<br>
<x-card>
<x-table striped :headers="$headers" :rows="$users" :sort-by="$sortBy" link="/admin/users/{id}/edit"
with-pagination>
@scope('cell_name', $user)
<x-avatar :image="Gravatar::get($user->email)">
<x-slot:title>
{{ $user->name }}
</x-slot:title>
</x-avatar>
@endscope
@scope('cell_valid', $user)
@if ($user->valid)
<x-icon name="o-check-circle" />
@endif
@endscope
@scope('cell_role', $user)
@if ($user->role === 'admin')
<x-badge value="{{ __('Administrator') }}" class="badge-error" />
@elseif($user->role === 'redac')
<x-badge value="{{ __('Redactor') }}" class="badge-warning" />
@elseif($user->role === 'user')
{{ __('User') }}
@endif
@endscope
@scope('cell_posts_count', $user)
@if ($user->posts_count > 0)
<x-badge value="{{ $user->posts_count }}" class="badge-primary" />
@endif
@endscope
@scope('cell_comments_count', $user)
@if ($user->comments_count > 0)
<x-badge value="{{ $user->comments_count }}" class="badge-success" />
@endif
@endscope
@scope('cell_created_at', $user)
{{ $user->created_at->isoFormat('LL') }}
@endscope
@scope('actions', $user)
<div class="flex">
<x-popover>
<x-slot:trigger>
<x-button
icon="o-envelope"
link="mailto:{{ $user->email }}"
no-wire-navigate
spinner
class="text-blue-500 btn-ghost btn-sm" />
</x-slot:trigger>
<x-slot:content class="pop-small">
@lang('Send an email')
</x-slot:content>
</x-popover>
<x-popover>
<x-slot:trigger>
<x-button
icon="o-trash"
wire:click="deleteUser({{ $user->id }})"
wire:confirm="{{ __('Are you sure to delete this user?') }}"
confirm-text="Are you sure?"
spinner
class="text-red-500 btn-ghost btn-sm" />
</x-slot:trigger>
<x-slot:content class="pop-small">
@lang('Delete')
</x-slot:content>
</x-popover>
</div>
@endscope
</x-table>
</x-card>
</div>
On a besoin d'un certain nombre de traductions :
"All": "Tous",
"Administrators": "Administrateurs",
"Redactors": "Rédacteurs",
"Administrator": "Administrateur",
"Redactor": "Rédacteur",
"Role": "Rôle",
"Valid": "Valide",
"Valid user": "Utilisateur validé",
"Send an email": "Envoyer un email",
"Are you sure to delete this user?": "Etes-vous sûr de vouloir supprimer cet utilisateur ?",
"Registration": "Inscription",
On a cet aspect global :
On peut filtrer par rôle (administrateur, rédacteur ou simple utilisateur) et on dispose du nombre global pour chaque rôle. On peut aussi trier sur les colonnes et on dispose d'une recherche sur les noms, ce qui est utile lorsqu'on a de nombreux utilisateurs.
Pour chaque utilisateur, on peut connaître : le nom, l'email, le rôle, la validité pour les commentaires, le nombre d'articles publiés, la date d'inscription. On dispose de deux actions : envoyer un email et supprimer le compte.
La requête expliquée
La requête pour récupérer les utilisateurs est relativement complexe et mérite quelques explications, en particulier pour les débutants qui souvent ont un peu du mal avec Eloquent.
public function users(): LengthAwarePaginator
{
// Le code de la fonction commence ici
}
Cette ligne définit une fonction publique nommée users qui retournera un objet de type LengthAwarePaginator (c'est un type de pagination dans Laravel).
$query = User::query()
->when($this->search, fn($q) => $q->where('name', 'like', "%{$this->search}%"))
->when($this->role !== 'all', fn($q) => $q->where('role', $this->role))
->withCount('posts', 'comments')
->orderBy(...array_values($this->sortBy));
Cette partie construit une requête pour récupérer les utilisateurs :
- User::query() commence une nouvelle requête sur le modèle User.
- ->when() ajoute des conditions à la requête si certaines conditions sont vraies.
- ->withCount() compte le nombre de posts et de commentaires pour chaque utilisateur.
- ->orderBy() trie les résultats selon les critères définis dans $this->sortBy.
$users = $query->paginate(10);
Cette ligne exécute la requête et pagine les résultats, avec 10 utilisateurs par page.
$userCountsByRole = User::selectRaw('role, count(*) as total')
->groupBy('role')
->pluck('total', 'role');
Cette partie compte le nombre d'utilisateurs pour chaque rôle.
$totalUsers = $userCountsByRole->sum();
Calcule le nombre total d'utilisateurs.
$this->roles = collect([
'all' => __('All') . " ({$totalUsers})",
'admin' => __('Administrators'),
'redac' => __('Redactors'),
'user' => __('Users'),
])
Crée une collection avec les différents rôles et leurs traductions.
->map(function ($roleName, $roleId) use ($userCountsByRole) {
$count = $userCountsByRole->get($roleId, 0);
return [
'name' => $roleId === 'all' ? $roleName : "{$roleName} ({$count})",
'id' => $roleId
];
})
Transforme chaque rôle pour inclure le nombre d'utilisateurs (sauf pour 'all').
->values()->all();
Convertit la collection en un tableau simple.
return $users;
Enfin, la fonction retourne les utilisateurs paginés.
En résumé, cette fonction récupère une liste paginée d'utilisateurs, en appliquant des filtres de recherche et de rôle si nécessaire. Elle compte également le nombre d'utilisateurs par rôle et prépare une liste de rôles avec leurs nombres respectifs d'utilisateurs.
Modifier un compte
Lorsqu'on clique sur une ligne du tableau, on doit accéder au formulaire de modification du compte. On a à nouveau besoin d'un composant Volt pour gérer ce formulaire et faire tout le traitement :
php artisan make:volt admin/users/edit --class
Il nous faut une route :
Route::middleware(IsAdmin::class)->group(function () {
...
Volt::route('/users/{user}/edit', 'admin.users.edit')->name('users.edit');
});
Et le code du composant :
<?php
use App\Models\User;
use Illuminate\Validation\Rule;
use Livewire\Attributes\{Layout, Title};
use Livewire\Volt\Component;
use Mary\Traits\Toast;
new #[Title('Edit User'), Layout('components.layouts.admin')]
class extends Component {
use Toast;
public User $user;
public string $name = '';
public string $email = '';
public string $role = '';
public bool $valid = false;
public bool $isStudent;
public function mount(User $user): void
{
$this->user = $user;
$this->fill($this->user);
}
public function save()
{
$data = $this->validate([
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'email', Rule::unique('users')->ignore($this->user->id)],
'role' => ['required', Rule::in(['admin', 'redac', 'user'])],
'valid' => ['required', 'boolean'],
]);
$this->user->update($data);
$this->success(__('User edited with success.'), redirectTo: '/admin/users/index');
}
public function with(): array
{
return [
'roles' => [['name' => __('Administrator'), 'id' => 'admin'], ['name' => __('Redactor'), 'id' => 'redac'], ['name' => __('User'), 'id' => 'user']],
];
}
}; ?>
<diV>
<x-header title="{{ __('Edit an account') }}" separator progress-indicator>
<x-slot:actions>
<x-button icon="s-building-office-2" label="{{ __('Dashboard') }}" class="btn-outline lg:hidden"
link="{{ route('admin') }}" />
</x-slot:actions>
</x-header>
<x-card>
<x-form wire:submit="save">
<x-input label="{{ __('Name') }}" wire:model="name" icon="o-user" inline />
<x-input label="{{ __('E-mail') }}" wire:model="email" icon="o-envelope" inline />
<br>
<x-radio label="{{ __('User role') }}" inline label="{{ __('Select a role') }}" :options="$roles"
wire:model="role" />
<br>
<x-toggle label="{{ __('Valid user') }}" inline wire:model="valid" />
<x-slot:actions>
<div class="text-right">
<x-button label="{{ __('Save') }}" icon="o-paper-airplane" spinner="save" type="submit"
class="btn-primary" />
</div>
</x-slot:actions>
</x-form>
</x-card>
</diV>
Il n'y a pas de nouveauté dans ce code qui reproduit ce que nous avons déjà vu pour les autres entités.
On ajoute ces trois traductions :
"Edit an account": "Modifier un compte",
"Select a role": "Sélectionnez un rôle",
"User edited with success.": "Utilisateur mis à jour avec succès.",
Et on a notre formulaire :
Vérifiez que tout fonctionne correctement.
Le tableau de bord
On va ajouter le lien du tableau des comptes à partir des statistiques du tableau de bord (admin.index) et on va en profiter pour ajouter aussi celui des pages que j'ai oublié dans l'article précédent :
@if (Auth::user()->isAdmin())
<a href="{{ route('pages.index') }}" class="flex-grow">
<x-stat title="{{ __('Pages') }}" value="{{ $pages->count() }}" icon="s-document"
class="shadow-hover" />
</a>
<a href="{{ route('users.index') }}" class="flex-grow">
<x-stat title="{{ __('Users') }}" value="{{ $users }}" icon="s-user"
class="shadow-hover" />
</a>
@endif
Vérifiez que les liens fonctionnent correctement sur les statistiques :
Conclusion
Notre gestion des comptes est désormais traitée entièrement. Dans la prochaine étape, on s'intéressera aux commentaires.
Pour vous simplifier la vie, vous pouvez charger le projet dans son état à l’issue de ce chapitre.
Par bestmomo
Nombre de commentaires : 5