Laravel

Un framework qui rend heureux

Voir cette catégorie
Vers le bas
Voir cette série
Mon CMS - Les favoris
Mercredi 4 septembre 2024 11:17

Souvent, nous visitons un site et découvrons des informations captivantes, en nous disant "il faut que je me rappelle ça !". Quelques jours plus tard, une nouvelle visite sur le site peut nous frustrer si nous ne parvenons pas à retrouver cette information parce qu'elle est perdue dans une masse de données pas forcément bien organisées. C'est précisément dans ce contexte qu'un système de favoris devient extrêmement pertinent. En permettant aux utilisateurs de simplement cliquer sur une petite étoile pour marquer une page, ils peuvent facilement revenir et retrouver l'information souhaitée, ce qui améliore considérablement leur expérience utilisateur.

Pour rappel, la table des matières est ici.

Les Avantages d'un Système de Favoris

Amélioration de l'expérience utilisateur

Cette fonctionnalité va bien au-delà d'un simple gadget. Elle transforme l'expérience de navigation en la rendant plus personnalisée et intuitive. Les utilisateurs peuvent créer leur propre bibliothèque de contenus préférés, ce qui les incite à revenir sur le site pour consulter leurs sélections.

Encouragement à l'inscription

Un système de favoris encourage les visiteurs à s'inscrire sur le site, car un visiteur anonyme ne peut évidemment pas mémoriser ses préférences. Cette inscription est bénéfique pour le site, parce qu'elle permet de créer une base d'utilisateurs engagés et fidèles.

Fidélisation des utilisateurs

En offrant la possibilité de sauvegarder des pages, le site incite les utilisateurs à revenir régulièrement. Cette fidélisation est cruciale pour maintenir une audience active et engagée.

Collecte de données précieuses

Un avantage indirect, mais significatif est la capacité à analyser les préférences des utilisateurs. En disposant de données sur les pages les plus souvent mises en favoris, le site peut obtenir des statistiques précieuses. Ces informations permettent d'identifier les contenus les plus appréciés et d'orienter les futures stratégies de contenu.

Mise en œuvre avec Laravel

La mise en place d'un système de favoris est également un excellent exercice de codage avec Laravel. Ce framework PHP offre une structure robuste et flexible qui facilite le développement de fonctionnalités complexes tout en restant accessible. Voici quelques raisons pour lesquelles Laravel est un bon choix :

  • simplicité et efficacité : Laravel permet de créer rapidement des fonctionnalités grâce à ses nombreuses bibliothèques intégrées et sa syntaxe élégante
  • sécurité : Le framework offre des mécanismes de sécurité intégrés, essentiels pour protéger les données des utilisateurs
  • extensibilité : Laravel est facilement extensible, ce qui permet d'ajouter de nouvelles fonctionnalités au système de favoris à mesure que les besoins évoluent

En somme, l'intégration d'un système de favoris sur un site web est une stratégie gagnante qui améliore l'expérience utilisateur, encourage l'inscription, fidélise les visiteurs, et fournit des données analytiques précieuses. Utiliser Laravel pour développer cette fonctionnalité est une approche judicieuse, combinant simplicité et puissance.

Les données

La migration

Pour mémoriser les favoris, il nous faut ajouter une table dans la base de données. Un utilisateur peut avoir plusieurs articles favoris et réciproquement un article peut être un favori de plusieurs utilisateurs. On est donc en présence d'une relation de type plusieurs à plusieurs et on a besoin d'une table pivot entre users et posts.

On commence par la migration :

php artisan make:migration create_favorites_table

Pour respecter les conventions d'Eloquent, on devrait nommer cette table post_user, mais on peut déroger à ces règles à condition d'en informer Eloquent quand on crée les relations.

Dans la table, il nous faut les clés étrangères des deux tables en relation :

public function up(): void
{
    Schema::create('favorites', function (Blueprint $table) {
        $table->id();
        $table->foreignId('user_id')->constrained()->onDelete('cascade');
        $table->foreignId('post_id')->constrained()->onDelete('cascade');
    });
}

Et bien sûr, on doit compléter la base :

php artisan migrate

Ce qui va ajouter la nouvelle table.

Les relations

Modèle User

Dans le modèle User on ajoute la relation :

use Illuminate\Database\Eloquent\Relations\{ HasMany, BelongsToMany };

...

public function favoritePosts(): BelongsToMany
{
    return $this->belongsToMany(Post::class, 'favorites');
}

On signale de façon explicite le nom de la table pivot puisqu'on n'a pas respecté les conventions.

Modèle Post

Dans le modèle Post, c'est la même chose :

use Illuminate\Database\Eloquent\Relations\{ HasMany, BelongsTo, BelongsToMany };

...

public function favoritedByUsers(): BelongsToMany
{
	return $this->belongsToMany(User::class, 'favorites');
}

Nous pourrons ainsi gérer les favoris à partir des utilisateurs ou des articles.

Le repository

Lorsqu'on récupère les informations d'un article et que l'utilisateur est connecté, on doit vérifier s'il a déjà mis l'article correspondant en favori, on complète dans le repository PostRepository la fonction getPostBySlug :

public function getPostBySlug(string $slug): Post
{
	$userId = auth()->id();
	
	return Post::with('user:id,name', 'category')
			->withCount('validComments')
			->withExists([
				'favoritedByUsers as is_favorited' => function ($query) use ($userId) {
					$query->where('user_id', $userId);
				},
			])
			->where('slug', $slug)->firstOrFail();
}


Post::with('user:id,name', 'category') : Cette ligne commence une requête pour récupérer un post avec ses relations.
    Post : Le modèle Post.
    with('user:id,name', 'category') : Charge les relations user et category avec le post.
        user:id,name : Charge uniquement les colonnes id et name de la relation user.
        category : Charge la relation category.

withCount('validComments') : Ajoute un comptage des commentaires valides associés au post.
    validComments : Le nom de la relation qui représente les commentaires valides.

withExists : Ajoute une condition pour vérifier si le post est favori pour l'utilisateur connecté.
    favoritedByUsers as is_favorited : Le nom de la relation qui représente les utilisateurs qui ont mis le post en favori.
    function ($query) use ($userId) : Une fonction anonyme qui prend une instance de $query et utilise la variable $userId.
        $query->where('user_id', $userId) : Ajoute une condition à la requête pour vérifier si l'utilisateur connecté a mis le post en favori.

where('slug', $slug) : Ajoute une condition à la requête pour filtrer les posts par leur slug.
    slug : Le nom de la colonne dans la table posts.
    $slug : La valeur du slug passée en argument à la fonction.

firstOrFail() : Exécute la requête et retourne le premier post correspondant. Si aucun post n'est trouvé, une exception ModelNotFoundException est levée.

Une étoile dans l'article

Pour qu'un utilisateur puisse mettre un article en favori, il faut ajouter un bouton dans chaque article. Ce bouton aura deux aspects selon que l'article a déjà été marqué en favori ou pas.

Le PHP

Dans le composant posts.show, on doit ajouter deux fonctions pour gérer le bouton :

public function favoritePost(): void
{
    $user = auth()->user();

    if ($user) {
        $user->favoritePosts()->attach($this->post->id);
        $this->post->is_favorited = true;
    }
}

public function unfavoritePost(): void
{
    $user = auth()->user();

    if ($user) {
        $user->favoritePosts()->detach($this->post->id);
        $this->post->is_favorited = false;
    }
}

On attache ou on détache un enregistrement dans la table pivot selon le cas.

Le HTML

    <div id="top" class="flex justify-end gap-4">
        @auth
            <x-popover>
                <x-slot:trigger>
                    @if ($post->is_favorited)
                        <x-button icon="s-star" wire:click="unfavoritePost" spinner
                            class="text-yellow-500 btn-ghost btn-sm" />
                    @else
                        <x-button icon="s-star" wire:click="favoritePost" spinner class="btn-ghost btn-sm" />
                    @endif
                </x-slot:trigger>
                <x-slot:content class="pop-small">
                    @if ($post->is_favorited)
                        @lang('Remove from favorites')
                    @else
                        @lang('Bookmark this post')
                    @endif
                </x-slot:content>
            </x-popover>
        @endauth

        ...

    </div>

Pour les utilisateurs connectés (@auth) on affiche le bouton en prévoyant les deux aspects.

Il ne reste plus qu'à ajouter els traductions :

"Bookmark this post": "Marquer cet article",
"Remove from favorites": "Supprimer des favoris"

Si l'article n'a pas été marqué comme favori :

Et s'il est marqué :

Vérifiez que tout ça fonctionne bien.

Trouver ses articles favoris

Le repository

Lorsqu'on charge les articles pour la page d'accueil, on doit ajouter l'information des favoris lorsqu'un utilisateur est connecté. On complète la fonction getBaseQuery :

use Illuminate\Support\Facades\DB;

...

protected function getBaseQuery(): Builder
{
	...

	return Post::select('id', 'slug', 'image', 'title', 'user_id', 'category_id', 'created_at', 'pinned')
		
        ...

    	->when(auth()->check(), function ($query) {
			$userId = auth()->id();
			$query->addSelect([
				'is_favorited' => DB::table('favorites')
					->selectRaw('1')
					->whereColumn('post_id', 'posts.id')
					->where('user_id', $userId)
					->limit(1)
			]);
		});	
}

when : Cette méthode permet d'exécuter une partie de la requête uniquement si une certaine condition est remplie.
auth()->check() : Cette méthode vérifie si un utilisateur est actuellement authentifié. Elle retourne true si un utilisateur est authentifié, sinon false.
function ($query) { ... } : Si la condition est remplie (c'est-à-dire si un utilisateur est authentifié), la fonction anonyme sera exécutée.   
     $query : La requête en cours.
$userId = auth()->id(); : Récupère l'ID de l'utilisateur authentifié.
$query->addSelect([ ... ]) : Ajoute une nouvelle colonne à la sélection.
    'is_favorited' => DB::table('favorites') : Ajoute une colonne nommée is_favorited à la sélection.
        DB::table('favorites') : Utilise la table favorites pour la sous-requête.
        ->selectRaw('1') : Sélectionne la valeur 1 si une ligne correspondante est trouvée dans la table favorites.
        ->whereColumn('post_id', 'posts.id') : Vérifie si le post_id dans la table favorites correspond à l'ID du post en cours.
        ->where('user_id', $userId) : Vérifie si le user_id dans la table favorites correspond à l'ID de l'utilisateur authentifié.
        ->limit(1) : Limite la sous-requête à une seule ligne.

La page d'accueil

On doit faire apparaître une étoile sur chaque article mis en favori sur la page d'accueil.

Dans le composant index, on ajoute le code :

<x-slot:menu>
    @if ($post->pinned)
        <x-badge value="{{ __('Pinned') }}" class="p-3 badge-warning" />
    @endif
    @auth
        @if ($post->is_favorited)
            <x-icon name="s-star" class="w-6 h-6 text-yellow-500 cursor-pointer" />
        @endif
    @endauth
</x-slot:menu>

Trouver tous ses favoris

On doit aussi permettre à un utilisateur de retrouver tous ses articles favoris, c'est d'ailleurs le but de la manœuvre !

Le repository

Dans le repository, on ajoute une fonction pour retrouver ces articles :

use App\Models\{Category, Post, User};

...

public function getFavoritePosts(User $user): LengthAwarePaginator
{
	return $this->getBaseQuery()
		->whereHas('favoritedByUsers', function (Builder $query) {
			$query->where('user_id', auth()->id());
		})
		->latest()
		->paginate(config('app.pagination'));
}

La route

On ajoute une route :

Route::middleware('auth')->group(function () {
	...
	Volt::route('/favorites', 'index')->name('posts.favorites');
});

La barre de navigation

On ajoute un bouton dans la barre de navigation (navigation.navbar) :

        @auth
            @if ($user->favoritePosts()->exists())
                <a title="{{ __('Favorites posts') }}" href="{{ route('posts.favorites') }}"><x-icon name="s-star"
                        class="w-7 h-7" /></a>
            @endif
        @endauth
        <x-theme-toggle title="{{ __('Toggle theme') }}" class="w-4 h-8" />
        <livewire:search />
    </x-slot:actions>
</x-nav>

Le composant index

Dans le composant index de la page d'accueil, on ajoute une propriété pour les favoris :

new class extends Component {

    ...

    public bool $favorites     = false;

Dans la fonction mount, on complète le code :

public function mount(string $slug = '', string $param = ''): void
{
    $this->param = $param;
    
    if (request()->is('category/*')) {
        $this->category = $this->getCategoryBySlug($slug);
    } elseif (request()->is('favorites')) {
        $this->favorites = true;
    }
}

On complète aussi la fonction getPosts :

public function getPosts(): LengthAwarePaginator
{
    ...

    if ($this->favorites) {
        return $postRepository->getFavoritePosts(auth()->user());
    }       
    return $postRepository->getPostsPaginate($this->category);
}

Et dans le HTML pour changer le titre :

@if ($category)
    <x-header title="{{ __('Posts for category ') }} {{ $category->title }}" size="text-2xl sm:text-3xl md:text-4xl" />
@elseif($param !== '')
    <x-header title="{{ __('Posts for search ') }} '{{ $param }}'" size="text-2xl sm:text-3xl md:text-4xl" />
@elseif($favorites)
    <x-header title="{{ __('Your favorites posts') }}" size="text-2xl sm:text-3xl md:text-4xl" />
@endif

On complète avec une traduction :

"Your favorites posts": "Vos articles favoris"

Des boutons pour scroller

On va terminer en ajoutant deux boutons pour aller rapidement en bas et en haut des articles, ce qui est souvent bien pratique.

Pour avoir un mouvement fluide, on ajoute une règle dans le fichier resources/css/app.css :

html {
    scroll-behavior: smooth;
}

Et les deux boutons dans posts.show :

      ...

    <div id="top" class="flex justify-end gap-4">

        ...

        <x-popover>
            <x-slot:trigger>
                <a href="#bottom"><x-icon name="c-arrow-long-down" /></a>
            </x-slot:trigger>
            <x-slot:content class="pop-small">
                @lang('To bottom')
            </x-slot:content>
        </x-popover>
    </div>

      ...

     <div id="bottom" class="relative flex justify-end w-full py-5 mx-auto md:px-12 max-w-7xl">
        <x-popover>
            <x-slot:trigger>
                <a href="#top"><x-icon name="c-arrow-long-up" />
            </x-slot:trigger>
            <x-slot:content class="pop-small">
                @lang('To up')
            </x-slot:content>
        </x-popover>
    </div>

</div>

On ajoute les deux traductions :

"To up": "Vers le haut",
"To bottom": "Vers le bas",

Conclusion

Notre CMS comporte maintenant une gestion complète des favoris !

Pour vous simplifier la vie, vous pouvez charger le projet dans son état à l’issue de ce chapitre.



Par bestmomo

Aucun commentaire

Article précédent : Mon CMS - Le profil
Article suivant : Mon CMS - L'administration