Dans cet article on va continuer à créer le compte client pour notre boutique. Après lui avoir permis de modifier ses données personnelles et nous êtres mis en conformité avec le RGPD nous allons à présent lui permettre de créer et modifier des adresses.
Le code présent dans cet article est un excellent exemple de gestion complète d'une ressource. Vous pouvez télécharger un ZIP du projet ici.Contrôleur et routes
Pour gérer les adresses on crée un contrôleur de ressource :php artisan make:controller AddressController --resource --model=Models\Address
On va conserver toutes les méthodes sauf show. On prévoit les routes :
Route::middleware('auth')->group(function () {
// Gestion du compte
Route::prefix('compte')->group(function () {
...
Route::resource('adresses', 'AddressController')->except('show');
On ajoute le lient dans la vue account.index :
<div class="col s12 m6"><a href="{{ route('adresses.index') }}" class="btn-large"><i class="material-icons left">location_on</i>Mes Adresses</a></div>
L'affichage des adresses
Pour afficher toutes les adresses on code la méthode index :public function index(Request $request)
{
$addresses = $request->user()->addresses()->with('country')->get();
if($addresses->isEmpty()) {
return redirect(route('adresses.create'))->with('message', config('messages.oneaddress'));
}
return view('account.addresses.index', compact('addresses'));
}
On récupère dans la base toutes les adresses du client connecté avec les pays associés. S'il n'y a pas d'adresse enregistrée on redirige sur la page de création d'une adresse. Sinon on ouvre une vue que l'on va maintenant créer :
@extends('layouts.app')
@section('content')
<div class="container">
<h2>Mes adresses</h2>
<div class="row">
@foreach($addresses as $address)
<div class="col s12 m6 l4">
<div class="card">
<div class="card-content address">
@include('account.addresses.partials.address')
</div>
<div class="card-action">
<a href="{{ route('adresses.edit', $address->id) }}">Mettre à jour</a>
<a class="delete" href="{{ route('adresses.destroy', $address->id) }}">Supprimer</a>
</div>
</div>
</div>
@endforeach
</div>
<div class="row">
@if(url()->previous() === route('commandes.create'))
<a class="waves-effect waves-light btn" href="{{ route('commandes.create') }}"> <i class="material-icons left">chevron_left</i>Retour à ma commande</a>
@else
<a class="waves-effect waves-light btn" href="{{ route('account') }}"> <i class="material-icons left">chevron_left</i>Retour à mon compte</a>
@endif
<a class="waves-effect waves-light btn" href="{{ route('adresses.create') }}">Créer une adresse</a>
</div>
</div>
@endsection
@section('javascript')
<script>
const del = async url => {
const response = await fetch(url, {
method: 'DELETE',
headers: { 'X-CSRF-TOKEN': '{{ csrf_token() }}' }
});
location.reload(true);
};
document.addEventListener('DOMContentLoaded', () => {
@if(session()->has('alert'))
M.toast({ html: '{{ session('alert') }}' });
@endif
const deleteButtons = document.querySelectorAll('.delete');
deleteButtons.forEach( button => {
button.addEventListener('click', e => {
e.preventDefault();
del(e.target.getAttribute('href'));
});
});
});
</script>
@endsection
Normalement la page doit s'afficher avec toutes les adresses :
Création d'une adresse
On code la méthode create :use App\Models\ { Address, Country };
...
public function create()
{
$countries = Country::all();
return view('account.addresses.create', compact('countries'));
}
Et on crée la vue :
@extends('layouts.app')
@section('content')
<div class="container">
<h2>Nouvelle adresse</h2>
@if(session()->has('message'))
<h5 class="center-align blue-text">{{ session('message') }}</h5>
<br>
@endif
<div class="row">
<div class="card" id="account">
<form method="POST" action="{{ route('adresses.store') }}">
@include('account.addresses.partials.form')
</form>
</div>
</div>
<div class="row">
<a class="waves-effect waves-light btn" href="{{ route('adresses.index') }}"> <i class="material-icons left">chevron_left</i>Retour à mes adresses</a>
</div>
</div>
@endsection
@section('javascript')
@include('account.addresses.partials.script')
@endsection
Le code est léger parce qu'on va utiliser deux vues partielles pour mutualiser le code avec la modification d'une adresse qui aura un formulaire similaire.
Une vue partielle pour le formulaire
On crée une vue partielle pour le formulaire :<div class="card-content">
@csrf
<div class="row col s12">
<label>
<input type="checkbox" name="professionnal" id="professionnal" {{ old('professionnal', isset($adress) ? $adress->professionnal : false) ? 'checked' : '' }}>
<span>C'est une adresse professionnelle</span>
</label>
</div>
<div class="row col s12">
<label>
<input name="civility" type="radio" value="Mme" {{ old('civility', isset($adress) ? $adress->civility : '') == 'Mme' ? 'checked' : '' }} />
<span>Mme.</span>
</label>
<label>
<input name="civility" type="radio" value="M." {{ old('civility', isset($adress) ? $adress->civility : '') == 'M.' ? 'checked' : '' }} />
<span>M.</span>
</label>
</div>
<x-input
name="name"
type="text"
icon="person"
label="Nom"
:value="isset($adress) ? $adress->name : ''"
></x-input>
<x-input
name="firstname"
type="text"
icon="person"
label="Prénom"
:value="isset($adress) ? $adress->firstname : ''"
></x-input>
<x-input
name="company"
type="text"
icon="business"
label="Raison sociale"
:value="isset($adress) ? $adress->company : ''"
></x-input>
<x-input
name="address"
type="text"
icon="home"
label="N° et libellé de la voie"
:value="isset($adress) ? $adress->address : ''"
required="true"
></x-input>
<x-input
name="addressbis"
type="text"
icon="home"
label="Bâtiment, Immeuble (optionnel)"
:value="isset($adress) ? $adress->addressbis : ''"
></x-input>
<x-input
name="bp"
type="text"
icon="location_on"
label="Lieu-dit ou BP (optionnel)"
:value="isset($adress) ? $adress->bp : ''"
></x-input>
<x-input
name="postal"
type="text"
icon="location_on"
label="Code postal"
:value="isset($adress) ? $adress->postal : ''"
required="true"
></x-input>
<x-input
name="city"
type="text"
icon="location_on"
label="Ville"
:value="isset($adress) ? $adress->city : ''"
required="true"
></x-input>
<div class="row">
<div class="input-field col s12">
<i class="material-icons prefix">location_on</i>
<select name="country_id"">
@foreach($countries as $country)
<option
value="{{ $country->id }}"
@if(old('country_id', isset($adress) ? $adress->country_id : '') == $country->id) selected @endif>{{ $country->name }}
</option>
@endforeach
</select>
<label>Pays</label>
</div>
</div>
<x-input
name="phone"
type="text"
icon="phone"
label="N° de téléphone"
:value="isset($adress) ? $adress->phone : ''"
required="true"
></x-input>
<p>
<button class="btn waves-effect waves-light" style="width: 100%" type="submit">
Enregistrer
</button>
</p>
</div>
On utilise le composant qu'on avait déjà créé.
Une vue partielle pour le script du formulaire
Pour bien organiser les choses on crée aussi une vue partielle pour le Javascript :<script>
const toggleProfessionnal = () => {
if(document.querySelector('#professionnal').checked) {
document.querySelector('#company').parentNode.parentNode.classList.remove('hide');
document.querySelector('label[for="name"]').firstChild.textContent = "Nom (optionnel)";
document.querySelector('label[for="firstname"]').firstChild.textContent = "Prénom (optionnel)";
} else {
document.querySelector('#company').parentNode.parentNode.classList.add('hide');
document.querySelector('label[for="name"]').firstChild.textContent = "Nom";
document.querySelector('label[for="firstname"]').firstChild.textContent = "Prénom";
}
}
document.addEventListener('DOMContentLoaded', () => {
toggleProfessionnal();
document.querySelector('#professionnal').addEventListener('click', () => toggleProfessionnal());
});
</script>
Le seul but de ce script sera de rendre des champs actifs ou non selon que c'est une adresse professionnelle ou pas.
Maintenant le formulaire doit s'afficher : Lorsque c'est une adresse professionnelle certains champs changent :Le nom est le prénom deviennent optionnels et un champ Raison sociale apparaît comme obligatoire.
La validation
On crée une requête de formulaire pour la validation :php artisan make:request StoreAddress
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StoreAddress 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 [
'name' => 'required_unless:professionnal,"on"|string|max:100',
'firstname' => 'required_unless:professionnal,"on"|string|max:100',
'company' => 'exclude_unless:professionnal,"on"|string|max:100',
'address' => 'required|string|max:255',
'addressbis' => 'nullable|string|max:255',
'bp' => 'nullable|string|max:100',
'postal' => 'required|numeric',
'city' => 'required|string|max:100',
'phone' => 'required|numeric',
];
}
}
Pour les champs name, firstname et company il faut tenir compte de la valeur de la case à cocher professionnal. Le reste est classique.
La création
Il ne resste plus qu'à code la méthode store du contrôleur :use App\Http\Requests\StoreAddress;
...
public function store(StoreAddress $storeAddress)
{
$storeAddress->merge(['professionnal' => $storeAddress->has('professionnal')]);
$storeAddress->user()->addresses()->create($storeAddress->all());
return redirect(route('adresses.index'))->with('alert', config('messages.addresssaved'));
}
On ajoute le champ professionnal qui est une case à cocher. On utilise la relation pour créer l'adresse avec toutes les entrées. Enfin on redirige avec un message. Donc il faut créer ce message dans config.messages et on va anticiper pour la suite :
return [
...
'addresssaved' => "L'adresse a bien été enregistrée.",
'addressupdated' => "L'adresse a bien été modifiée.",
'addressdeleted' => "L'adresse a bien été supprimée.",
];
On peut maintenant créer une adresse :
Il y a toutefois un peti souci avec les textes de la validation :
Ce n'est pas très explicite et élégant !
Si on regarde dans la fichier resources/lang/fr/validation.php on a :
'attributes' => [
...
'first_name' => 'prénom',
On peut déjà changer ça :
'attributes' => [
...
'firstname' => 'prénom',
On va aussi ajouter :
'professionnal' => 'adresse professionnelle'
'company' => 'raison sociale',
C'est déjà mieux :
C'est la fin qui n'est pas très parlante, une valeur on...
On va ajouter ça dans le menu des traductions :
'values' => [
'professionnal' => [
'on' => 'active',
],
],
Maintenant c'est parfait !
Modification d'une adresse
Une autorisation
On va s'arranger pour que seul le propriétaire d'une adresse puisse la modifier (et aussi pour la supprimer) :php artisan make:policy AddressPolicy
<?php
namespace App\Policies;
use App\Models\ { User, Address };
use Illuminate\Auth\Access\HandlesAuthorization;
class AddressPolicy
{
use HandlesAuthorization;
/**
* Create a new policy instance.
*
* @return void
*/
public function manage(User $user, Address $address)
{
return $user->id === $address->user_id;
}
}
Et dans AuthServiceProvider :
use App\Models\{ Address, Order };
use App\Policies\{ AddressPolicy, OrderPolicy };
...
protected $policies = [
Address::class => AddressPolicy::class,
...
];
La méthode edit
Dans le contrôleur on code la méthode edit :public function edit(Address $adress)
{
$this->authorize('manage', $adress);
$countries = Country::all();
return view('account.addresses.edit', compact('adress', 'countries'));
}
La vue
On ajoute la vue : Et là on bénéficie de ce qu'on a codé auparavent :@extends('layouts.app')
@section('content')
<div class="container">
<h2>Mettre à jour mon adresse</h2>
<div class="row">
<div class="card">
<form method="POST" action="{{ route('adresses.update', $adress->id) }}">
@method('PUT')
@include('account.addresses.partials.form')
</form>
</div>
</div>
<div class="row">
<a class="waves-effect waves-light btn" href="{{ route('adresses.index') }}"> <i class="material-icons left">chevron_left</i>Retour à mes adresses</a>
</div>
</div>
@endsection
@section('javascript')
@include('account.addresses.partials.script')
@endsection
La méthode update
Il ne reste plus qu'à coder la méthode update pour mémoriser les changements :
public function update(StoreAddress $storeAddress, Address $adress)
{
$this->authorize('manage', $adress);
$storeAddress->merge(['professionnal' => $storeAddress->has('professionnal')]);
$adress->update($storeAddress->all());
return redirect(route('adresses.index'))->with('alert', config('messages.addressupdated'));
}
On utilise la même validation que pour la création. On lance un petit toast pour informer le client que les changements ont bien été effectués :
Suppression d'une adresse
Pour supprimer une adresse on code la méthode destroy :public function destroy(Address $adress)
{
$this->authorize('manage', $adress);
$adress->delete();
return back()->with('alert', config('messages.addressdeleted'));
}
Là aussi on lance un toast d'information :
Petit retour sur les commandes
Lorsqu'on a codé la méthdoe create du contrôleur OrderController on a laissé un petit commentaire :public function create(Request $request, Shipping $ship)
{
$addresses = $request->user()->addresses()->get();
if($addresses->isEmpty()) {
// Là il faudra renvoyer l'utilisateur sur son compte quand on l'aura créé
}
Ca a d'ailleurs donné lieu à des questions parce qu'il y a forcément un bug lorsqu'un client n'a pas encore créé d'adresse. On va maintenant mettre en place une redirection :
if($addresses->isEmpty()) {
return redirect()->route('adresses.create')->with('message', 'Vous devez créer au moins une adresse pour passer une commande.');
}
Maintenant quand un client sans adresse veut faire une commande il est dirigé sur la page de création d'une adresse avec un message :
Conclusion
On en a terminé avec la gestion des adresses. Il ne nous restera plus qu'à prévoir la gestion des commandes pour terminer cette partie qui concerne le compte du client.
Par bestmomo
Nombre de commentaires : 6