Laravel 8

Créer un blog – contact et pages

Nous avons dans le précédent article mis en place toutes les vues de l’authentification et on en a profité pour compléter la barre de navigation pour créer les liens adaptés. Dans le présent article on va installer un formulaire de contact en considérant deux cas : l’utilisateur est authentifié et on connait déjà son nom et son email, ou il ne l’est pas et alors on va lui demander ces renseignements. Ensuite on verra la partie CMS avec des pages.

Vous pouvez télécharger le code final de cet article ici.

Le formulaire de contact

Pour le formulaire de contact on prévoit ces données :

  • le nom
  • l’email
  • le message

Mais si l’utilisateur est authentifié on va aussi prévoir une clé étrangère pour se référer à la table users.

Comme on l’a fait pour les contacts on crée simultanément le modèle, le factory, le contrôleur et la migration :

php artisan make:model Contact -mfc

Ce qui est dommage c’est que le contrôleur ne se positionne pas d’office dans le dossier Front. Il faut donc le déplacer comme on l’a déjà fait pour les articles et les commentaires (attention à l’espace de nom !) :

Pour le factory c’est bon :

Pour le modèle aussi :

De même que la migration :

La migration

On va coder cette migration :

public function up()
{
    Schema::create('contacts', function(Blueprint $table) {
        $table->id();
        $table->timestamps();
        $table->unsignedBigInteger('user_id')->nullable();
        $table->string('name');
        $table->string('email');
        $table->text('message');
    });
}

On prévoit une clé étrangère non obligatoire qui ne servira que pour les utilisateur inscrits et authentifiés au moment où ile prennent contact.

Le modèle Contact

Dans le modèle on ajoute la propriété $fillable ainsi que la relation qui nous permettra au besoin de retrouver l’utilisateur associé, on ajoute aussi le trait Notifiable :

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Notifications\Notifiable;

class Contact extends Model
{
    use HasFactory, Notifiable;

    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = ['name', 'email', 'message', 'user_id'];

    /**
     * Get user of the Contact
     *
     * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
     */
    public function user()
    {
        return $this->belongsTo(User::class);
    }
}

La population

On code la factory ContactFactory :

public function definition()
{
    return [
        'name' => $this->faker->name,
        'email' => $this->faker->unique()->safeEmail,
        'message' => $this->faker->realText($maxNbChars = 200, $indexSize = 2),
    ];
}

Et dans DatabaseSeeder on génère 5 contacts :

// Contacts
Contact::withoutEvents(function () {
    Contact::factory()->count(5)->create();
});

Vous pouvez régénérer toute la base (on est obligé pour avoir la population) :

php artisan migrate:fresh --seed

Vérifiez que vous avez bien généré les 5 contacts :

Pour le moment ça ne sert pas à grand chose mais on sera prêt pour la partie administration.

La validation

On crée une form request :

php artisan make:request Front\ContactRequest

<?php

namespace App\Http\Requests\Front;

use Illuminate\Foundation\Http\FormRequest;

class ContactRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize()
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
        return  [
            'message' => 'required|max:1000',
            'name' => 'sometimes|required|string|max:255',
            'email' => 'sometimes|required|string|email|max:255',
        ];
    }
}

Remarquez l’utilisation de sometimes qui permet de ne tenir compte du champ que s’il est présent dans la requête, ça nous arrange pour name et email qui ne sont pas forcément présent.

Le contrôleur et les routes

Dans le contrôleur on a besoin de deux fonctions :

<?php

namespace App\Http\Controllers\Front;

use App\Http\Controllers\Controller;
use App\Http\Requests\Front\ContactRequest;
use App\Models\Contact;

class ContactController extends Controller
{
    public function create()
    {
        return view('front.contact');
    }

    public function store(ContactRequest $request)
    {
        if($request->user()) {
            $request->merge([
                'user_id' => $request->user()->id,
                'name'    => $request->user()->name,
                'email'   => $request->user()->email,
            ]);
        }

        Contact::create ($request->all());

        return back()->with ('status', __('Your message has been recorded, we will respond as soon as possible.'));
    }
}

La première fonction (create) se contente d’appeler la vue avec le formulaire.

La seconde (store) est là pour mémoriser les informations. Si l’utilisateur est authentifié on récupère les informations le concernant. On pourrait évidemment aller chercher ultérieurement ces informations dans la table users mais ça nous simplifiera la vie dans l’administration de les avoir directement dans la table contacts.

Pour les routes on crée une ressource partielle :

use App\Http\Controllers\Front\{
    ...
    ContactController as FrontContactController
};

...

Route::resource('contacts', FrontContactController::class, ['only' => ['create', 'store']]);

La vue

Il ne nous manque plus que la vue :

@extends('front.layout')

@section('main')

    <div class="row">
        <div class="column large-12">

            <div class="s-content__entry-header">
                <h1 class="s-content__title">@lang('Get In Touch With Us')</h1>
            </div>

            <div class="row row-x-center">
                <div class="column large-6 tab-12">

                  <!-- Session Status -->
                  <x-auth.session-status :status="session('status')" />

                  <!-- Validation Errors -->
                  <x-auth.validation-errors :errors="$errors" />

                  <form class="s-content__form" method="post" action="{{ route('contacts.store') }}">
                      @csrf
                      <fieldset>

                          @if(Auth::guest())
                              <!-- Name -->
                              <div>
                                <label for="name">@lang('Name')</label>  
                                <input 
                                    id="name" 
                                    class="h-full-width" 
                                    type="text" 
                                    name="name" 
                                    placeholder="@lang('Your name')" 
                                    value="{{ old('name') }}" 
                                    required 
                                    autofocus>
                              </div>

                              <!-- Email Address -->
                              <x-auth.input-email />
                          @endif

                          <!-- Message -->                          
                          <label for="message">@lang('Your Message')</label> 
                          <textarea name="message" id="message" class="h-full-width" placeholder="@lang('Your Message')" required>{{ old('message') }}</textarea>                          

                          <br>
                          <x-auth.submit title="Send" />

                      </fieldset>
                  </form>

                </div>
            </div>


        </div>
    </div>

@endsection

Tant qu’à faire on profite des composants qu’on a créés pour l’authentification.

Le menu

Il ne nous manque plus qu’un lien dans le menu (front.layout) :

<ul class="s-header__nav">
    ...
    <li {{ currentRoute('contacts.create') }}>
        <a href="{{ route('contacts.create') }}" title="">@lang('Contact')</a>
    </li>
    @guest

Aspect

Pour un utilisateur non authentifié on a tous les champs :

Et s’il est authentifié c’est plus léger puisqu’on a déjà les informations :

On peut vérifier le fonctionnement de la validation :

Si la validation se passe bien on a cette alerte :

Dans le cas d’un utilisateur authentifié on a l’identifiant en plus des autres informations :

Sinon ce champ est NULL.

Les pages

Dans un site, quel qu’il soit, on a habituellement des pages pour présenter par exemple les données légales, ou les conditions de vente pour un site de commerce en ligne. Il faut donc presque toujours mettre en place une partie CMS, même légère. On va donc le faire pour le blog.

Pour le formulaire de contact on prévoit ces données :

  • le titre
  • le slug
  • le contenu

On crée simultanément le modèle, le factory, le contrôleur et la migration :

php artisan make:model Page -mfc

On va aussi déplacer le contrôleur (en ajustant l’espace de nom) :

La migration

On va coder la migration :

public function up()
{
    Schema::create('pages', function (Blueprint $table) {
        $table->id();
        $table->string('slug');
        $table->string('title');
        $table->text('body');
    });
}

On ne vas pas avoir besoin de dates pour cette table.

Le modèle Page

Dans le modèle on ajoute la propriété $fillable et on précise qu’on à pas de dates à gérer :

class Page extends Model
{
    use HasFactory;

    protected $fillable = [
        'title', 'slug', 'body',
    ];

    public $timestamps = false;
}

La population

On code la factory PageFactory :

public function definition()
{
    return [
        'body' => $this->faker->paragraph(10),
    ];
}

Et dans DatabaseSeeder on génère 4 pages :

// Pages
$items = [
    ['about-us', 'About us'],
    ['terms', 'Terms'],
    ['faq', 'FAQ'],
    ['privacy-policy', 'Privacy Policy'],
];

foreach($items as $item) {
    Page::factory()->create([
        'slug' => $item[0],
        'title' => $item[1],
    ]);
}

Vous pouvez régénérer toute la base (on est obligé pour avoir la population) :

php artisan migrate:fresh --seed

Vérifiez que vous avez bien généré les 4 pages :

Le contrôleur et la route

Pour le moment on ne va pas créer des pages, juste les afficher donc on simplifie au maximum le contrôleur Front/PageController :

<?php

namespace App\Http\Controllers\Front;

use App\Http\Controllers\Controller;
use App\Models\Page;

class PageController extends Controller
{
    public function __invoke(Page $page)
    {
        return view('front.page', compact('page'));
    }
}

Pour la route :

use App\Http\Controllers\Front\{
    ...
    PageController as FrontPageController
};

...

Route::name('page')->get('page/{page:slug}', FrontPageController::class);

La vue

On finit en ajoutant la vue :

@extends('front.layout')

@section('main')

    <div class="row">
        <div class="column large-12">

          <article class="s-content__entry">

            <div class="s-content__entry-header">
                <h1 class="s-content__title">@lang($page->title)</h1>
            </div>

            <div class="s-content__primary">

                <div class="s-content__page-content">

                    {!! $page->body !!}

                </div>

            </div>
        </article>

        </div>
    </div>

@endsection

Maintenant les pages doivent s’afficher avec l’url monblog.ext/page/slug.

Le footer

Maintenant qu’on a des pages on va modifier le footer pour insérer des liens vers ces pages.

On a déjà créer un composeur de vue pour le layout (app\Http\ViewComposers\HomeComposer) pour envoyer systématiquement les catégories. On va aussi envoyer les pages :

use App\Models\{ Category, Page };

...

public function compose(View $view)
{
    $view->with([
        'categories' => Category::has('posts')->get(),
        'pages'      => Page::select('slug', 'title')->get(),
    ]);
}

On évite de charger les contenus qui peuvent être volumineux.

Dans le footer on va supprimer le formulaire de contact qui ne servira pas, épurer un peu les données et surtout ajouter les liens vers nos pages :

<footer class="s-footer">

    <div class="s-footer__main">

        <div class="row">

            <div class="column large-6 medium-6 tab-12 s-footer__info">

                <h5>@lang('About Our Site')</h5>

                <p>
                Lorem ipsum Ut velit dolor Ut labore id fugiat in ut 
                fugiat nostrud qui in dolore commodo eu magna Duis 
                cillum dolor officia esse mollit proident Excepteur 
                exercitation nulla. Lorem ipsum In reprehenderit 
                commodo aliqua irure.
                </p>

            </div>

            <div class="column large-3 medium-3 tab-6 s-footer__site-links">

                <h5>@lang('Site Links')</h5>

                <ul>
                    @foreach($pages as $page)
                        <li><a href="{{ route('page', $page->slug) }}">@lang($page->title)</a></li>
                    @endforeach
                </ul>

            </div> 

            <div class="column large-2 medium-3 tab-6 s-footer__social-links">

                <h5>Follow Us</h5>

                <ul>
                    <li><a href="#0">Twitter</a></li>
                    <li><a href="#0">Facebook</a></li>
                    <li><a href="#0">Dribbble</a></li>
                    <li><a href="#0">Pinterest</a></li>
                    <li><a href="#0">Instagram</a></li>
                </ul>

            </div>

        </div>

    </div>

    <div class="s-footer__bottom">
        <div class="row">
            <div class="column">
                <div class="ss-copyright">
                    <span>© Copyright Calvin 2020</span> 
                    <span>Design by <a href="https://www.styleshout.com/">StyleShout</a></span>
                </div>
            </div>
        </div> 

        <div class="ss-go-top">
            <a class="smoothscroll" title="Back to Top" href="#top">
                <svg viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg" width="15" height="15"><path d="M7.5 1.5l.354-.354L7.5.793l-.354.353.354.354zm-.354.354l4 4 .708-.708-4-4-.708.708zm0-.708l-4 4 .708.708 4-4-.708-.708zM7 1.5V14h1V1.5H7z" fill="currentColor"></path></svg>
            </a>
        </div>
    </div>

</footer>

Vous n’avez plus qu’à vérifier que les liens des pages fonctionnent.

Conclusion

On avance… Dans le prochain article on se penchera sur les liens sociaux qui apparaissent dans le diaporama et au bas de la page. On créera aussi un événement pour mémoriser la création des articles, utilisateurs, contacts et commentaires, pour signaler tout ça ensuite à l’administrateurs et éventuellement aux rédacteurs. On créera aussi des pages d’erreurs en harmonie avec le blog. Et pour terminer on ajoutera les traductions françaises.

Print Friendly, PDF & Email

4 commentaires

  • Everyways

    Bonjour,

    Tuto très complet, merci !
    j’ai une petite galére, tout est ok et fonctionnel hormis 1 chose :
    La suppression des « socials » et des « contacts ». Lors du click depuis l’admin il me display une erreur « The GET method is not supported for this route. »
    Je pense que j’ai zappé une partie mais je ne trouve pas laquelle…

  • fabBlab

    Je rencontre un bug étrange lorsque je lance la création d’un script Request via artisan :
    php artisan make:request Front\ContactRequest

    J’obtiens en fait un fichier FrontContactRequest.php à la racine du dossier /Request.

    Pourtant le dossier Front existe déjà et est accessible en écriture.
    Bref, je suis bon à chaque fois pour renommer le fichier, le déplacer dans /Front et adapter l’espace de nom !

    Je n’ai pas le même souci pour la création des contrôleurs, migrations, etc.

Laisser un commentaire