Désormais, nos clients peuvent créer un compte, se connecter et même récupérer leur mot de passe. Cependant, il nous reste encore à développer les fonctionnalités permettant aux clients de mettre à jour leurs informations personnelles, de gérer leurs adresses, d'accéder aux détails de leurs commandes et de consulter toutes leurs données, afin de satisfaire les exigences du RGPD.
Le code complet à la fin de cet article peut être téléchargé ici.
Le profil
Lors de son inscription, un client donne un certain nombre de renseignements personnels. Il doit en conserver la maîtrise et pouvoir apporter des changements au besoin. En particulier, il doit pouvoir changer son nom, son prénom, son email, son mot de passe et son inscription à la lettre d'information. On va en conséquence créer un formulaire pour lui permettre d'effectuer ces changements.
On crée un nouveau composant Livewire :
php artisan make:livewire pages::account.profile

Avec ce code :
<?php
use App\Models\User;
use Illuminate\Validation\Rules\Password;
use Livewire\Features\SupportRedirects\Redirector;
use Illuminate\Support\Facades\{Auth, Hash};
use Illuminate\Support\Str;
use Illuminate\Validation\Rule;
use Livewire\Attributes\Title;
use Livewire\Component;
use Mary\Traits\Toast;
new #[Title('Profil')]
class extends Component {
use Toast;
public User $user;
public string $firstname = '';
public string $name = '';
public string $email = '';
public bool $newsletter = false;
public string $password = '';
public string $password_confirmation = '';
public bool $rgpd = false;
public function mount(): void
{
$this->user = Auth::user();
$this->firstname = $this->user->firstname ?? '';
$this->name = $this->user->name ?? '';
$this->email = $this->user->email ?? '';
$this->newsletter = (bool) ($this->user->newsletter ?? false);
}
public function generatePassword(): void
{
$generated = Str::password(14);
$this->password = $generated;
$this->password_confirmation = $generated;
$this->info(__('Nouveau mot de passe généré.'), position: 'toast-bottom');
}
public function save(): void
{
$data = $this->validate([
'firstname' => ['required', 'string', 'max:255'],
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'email', Rule::unique('users')->ignore($this->user->id)],
'newsletter' => ['boolean'],
'rgpd' => ['accepted'],
'password' => [
'nullable',
'confirmed',
Password::min(8)
->mixedCase() // Majuscules & Minuscules
->numbers() // Chiffres
->symbols() // Caractères spéciaux
->uncompromised(), // Vérifie via "Have I Been Pwned" si le mot de passe a fuité sur le web !
],
], [
'rgpd.accepted' => __('Vous devez accepter la politique de confidentialité pour enregistrer.')
]);
if (empty($data['password'])) {
unset($data['password']);
} else {
$data['password'] = Hash::make($data['password']);
}
unset($data['rgpd']);
$this->user->update($data);
$this->success(__('Profil mis à jour avec succès !'), redirectTo: route('home'));
}
public function cancel(): Redirector
{
return redirect()->route('home');
}
}; ?>
<div class="min-h-screen py-8 bg-base-200/50">
<div class="max-w-3xl mx-auto px-4">
{{-- En-tête --}}
<div class="text-center mb-8">
<h1 class="text-3xl font-extrabold tracking-tight">@lang('Mon profil')</h1>
<p class="text-sm text-base-content/70 mt-1">@lang('Gérez vos informations personnelles et vos préférences')</p>
</div>
{{-- Formulaire Carte --}}
<x-card class="shadow-xl bg-base-100 border border-base-300">
<x-form wire:submit="save" class="space-y-6">
{{-- Section : Informations personnelles --}}
<div class="space-y-4">
<div class="flex items-center gap-2 text-lg font-semibold border-b border-base-200 pb-2">
<x-icon name="o-user" class="w-5 h-5 text-primary" />
<span>@lang('Informations personnelles')</span>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<x-input
label="{{ __('Prénom') }}"
wire:model="firstname"
icon="o-user"
required
/>
<x-input
label="{{ __('Nom') }}"
wire:model="name"
icon="o-user"
required
/>
</div>
<x-input
label="{{ __('Email') }}"
wire:model="email"
icon="o-envelope"
type="email"
required
/>
</div>
{{-- Section : Mot de passe --}}
<div class="space-y-4 pt-4">
<div class="flex items-center gap-2 text-lg font-semibold border-b border-base-200 pb-2">
<x-icon name="o-lock-closed" class="w-5 h-5 text-primary" />
<span>@lang('Sécurité')</span>
</div>
<p class="text-xs text-base-content/60">
@lang('Laissez vide si vous ne souhaitez pas modifier votre mot de passe.')
</p>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<x-input
label="{{ __('Nouveau mot de passe') }}"
wire:model="password"
icon="o-key"
type="password"
/>
<x-input
label="{{ __('Confirmer le mot de passe') }}"
wire:model="password_confirmation"
icon="o-key"
type="password"
/>
</div>
<div class="flex items-center gap-3">
<x-button
label="{{ __('Générer un mot de passe sécurisé') }}"
wire:click="generatePassword"
icon="o-arrow-path"
class="btn-outline btn-sm btn-secondary"
type="button"
/>
</div>
{{-- Affichage temporaire du MDP généré --}}
@if($password)
<div x-data="{ showPass: false }" class="p-3 bg-warning/10 border border-warning/30 rounded-lg space-y-2">
<div class="flex items-center justify-between">
<span class="font-mono text-sm font-semibold">
<span x-show="!showPass">••••••••••••</span>
<span x-show="showPass" x-cloak>{{ $password }}</span>
</span>
<div class="flex items-center gap-2">
<button type="button" @click="showPass = !showPass" class="btn btn-ghost btn-xs">
<x-icon name="o-eye" class="w-4 h-4" />
</button>
<button type="button" onclick="navigator.clipboard.writeText('{{ $password }}')" class="btn btn-warning btn-xs">
<x-icon name="o-clipboard-document" class="w-4 h-4" />
@lang('Copier')
</button>
</div>
</div>
<p class="text-xs text-base-content/70">
@lang('Pensez à copier le mot de passe généré avant d\'enregistrer.')
</p>
</div>
@endif
</div>
{{-- Section : Préférences & RGPD --}}
<div class="space-y-4 pt-4">
<div class="flex items-center gap-2 text-lg font-semibold border-b border-base-200 pb-2">
<x-icon name="o-cog-6-tooth" class="w-5 h-5 text-primary" />
<span>@lang('Préférences et confidentialité')</span>
</div>
<div class="space-y-3 bg-base-200/50 p-4 rounded-lg">
<x-checkbox
label="{{ __('Je souhaite recevoir la newsletter') }}"
wire:model="newsletter"
/>
<x-checkbox
wire:model="rgpd"
>
<x-slot:label>
@lang('J\'accepte la')
<a href="{{ route('pages', ['page' => 'politique-de-confidentialite']) }}" target="_blank" class="link link-primary">
@lang('politique de confidentialité')
</a>
</x-slot:label>
</x-checkbox>
</div>
</div>
{{-- Actions --}}
<div class="flex flex-col-reverse sm:flex-row sm:items-center sm:justify-between gap-3 pt-6 border-t border-base-200">
<x-button
label="{{ __('Annuler') }}"
wire:click="cancel"
class="btn-ghost"
type="button"
/>
<x-button
label="{{ __('Enregistrer les modifications') }}"
icon="o-check"
spinner="save"
type="submit"
class="btn-primary"
::disabled="!$wire.rgpd"
/>
</div>
</x-form>
</x-card>
</div>
</div>
La route dans un groupe d'utilisateurs authentifiés et dans un groupe spécifique pour le compte utilisateur :
Route::middleware('auth')->group(function () {
Route::prefix('account')->group(function () {
Route::livewire('/profile', 'pages::account.profile')->name('profile');
});
});
On renseigne la barre de navigation :
<x-menu class="p-2 bg-base-100 text-base-content rounded-box shadow-2xl min-w-56 space-y-1">
<x-menu-item
title="{{ __('Mon profil') }}"
link="{{ route('profile') }}"
icon="o-user"
/>
Et la barre latérale :
<x-menu-item title="{{ __('Mon profil') }}" icon="o-user" link="{{ route('profile') }}" />
Et on a notre formulaire :

Comme dans le formulaire d'inscription, on prévoit une possibilité de générer un mot de passe sécurisé.
Les adresses
Le client doit aussi disposer d'une gestion complète de ses adresses : ajout, modification, suppression. La plupart des clients n'en auront qu'une qui servira pour l'expédition et la facturation. Mais ces clients professionnels peuvent vouloir les distinguer. D'autre part, on peut imaginer des expéditions à différentes adresses.
Un composant pour afficher une adresse
Comme il faudra afficher des adresses sur diverses pages, on prévoit un composant Blade :
php artisan make:component address --view

Avec ce code :
<div class="space-y-1 text-sm text-base-content/90 leading-relaxed">
{{-- Nom & Prénom ou Raison Sociale --}}
@if ($address->company)
<p class="font-bold text-base text-base-content">
{{ $address->company }}
</p>
@endif
@if ($address->name || $address->firstname)
<p class="font-semibold text-base-content">
{{ trim("{$address->civility} {$address->name} {$address->firstname}") }}
</p>
@endif
{{-- Voies & Adresses --}}
<div class="pt-0.5 space-y-0.5">
<p>{{ $address->address }}</p>
@if ($address->addressbis)
<p class="text-base-content/80">{{ $address->addressbis }}</p>
@endif
@if ($address->bp)
<p class="text-base-content/80">{{ $address->bp }}</p>
@endif
<p class="font-medium">
{{ trim("{$address->postal} {$address->city}") }}
</p>
@if ($address->country)
<p class="font-bold uppercase text-xs tracking-wider text-base-content/70 pt-1">
{{ $address->country->name }}
</p>
@endif
</div>
{{-- Numéro de téléphone --}}
@if ($address->phone)
<div class="flex items-center gap-1.5 pt-2 text-xs text-base-content/70 font-mono">
<x-icon name="o-phone" class="w-3.5 h-3.5 text-primary shrink-0" />
<span>{{ $address->phone }}</span>
</div>
@endif
</div>
Les routes
On ajoute les trois routes qui vont nous être utiles (affichage des adresses, création et modification) :
Route::middleware('auth')->group(function () {
Route::prefix('account')->group(function () {
...
Route::livewire('/addresses', 'pages::account.addresses.index')->name('addresses');
Route::livewire('/addresses/create', 'pages::account.addresses.create')->name('addresses.create');
Route::livewire('/addresses/{address}/edit', 'pages::account.addresses.edit')->name('addresses.edit');
});
});
Et on renseigne la barre de navigation et la barre latérale avec le même code :
<x-menu-item
title="{{ __('Mes adresses') }}"
link="{{ route('addresses') }}"
icon="o-map-pin"
/>
L'affichage des adresses
On va commencer par créer l'affichage de toutes les adresses présentes avec un nouveau composant :
php artisan make:livewire pages::account.addresses.index

Avec ce code :
<?php
use App\Models\Address;
use Livewire\Component;
use Illuminate\Support\Collection;
use Livewire\Attributes\Title;
use Illuminate\Support\Facades\Auth;
new #[Title('Mes adresses')]
class extends Component {
public function deleteAddress(Address $address): void
{
// Sécurité : Vérifier que l'adresse appartient bien à l'utilisateur connecté
if ($address->user_id !== Auth::id()) {
abort(403);
}
$address->delete();
$this->success(__('Adresse supprimée avec succès.'));
}
public function with(): array
{
return [
'addresses' => Auth::user()->addresses()->with('country')->get(),
];
}
}; ?>
<div class="max-w-5xl mx-auto py-8 px-4 space-y-6">
{{-- En-tête de la page --}}
<x-header title="{{ __('Mes adresses') }}" subtitle="{{ __('Gérez vos adresses de livraison et de facturation') }}" separator>
<x-slot:actions>
<x-button
label="{{ __('Ajouter une adresse') }}"
icon="o-plus"
link="{{ route('addresses.create') }}"
class="btn-primary btn-sm"
/>
</x-slot:actions>
</x-header>
{{-- Liste des adresses --}}
@if($addresses->isNotEmpty())
<div class="grid gap-6 grid-cols-1 md:grid-cols-2">
@foreach ($addresses as $address)
<x-card
class="shadow-sm hover:shadow-md transition-all duration-200 border border-base-200 hover:border-primary/50 bg-base-100"
>
{{-- Composant d'affichage de l'adresse --}}
<div class="pb-2">
<x-address :address="$address" />
</div>
<x-slot:actions class="pt-2 border-t border-base-200 justify-end gap-1">
<x-button
icon="o-pencil"
link="{{ route('addresses.edit', $address) }}"
class="btn-ghost btn-circle btn-xs text-base-content/70 hover:text-primary"
tooltip="{{ __('Modifier') }}"
/>
<x-button
icon="o-trash"
wire:click="deleteAddress({{ $address->id }})"
wire:confirm="{{ __('Êtes-vous sûr de vouloir supprimer cette adresse ?') }}"
spinner="deleteAddress({{ $address->id }})"
class="btn-ghost btn-circle btn-xs text-error"
tooltip="{{ __('Supprimer') }}"
/>
</x-slot:actions>
</x-card>
@endforeach
</div>
@else
{{-- État vide (Aucune adresse) --}}
<x-card class="text-center py-12 bg-base-100 shadow-sm border border-base-200">
<div class="flex flex-col items-center justify-center space-y-4">
<div class="w-16 h-16 rounded-full bg-primary/10 flex items-center justify-center text-primary">
<x-icon name="o-map-pin" class="w-8 h-8" />
</div>
<div class="space-y-1">
<h3 class="font-bold text-lg">{{ __('Aucune adresse enregistrée') }}</h3>
<p class="text-sm text-base-content/60">
{{ __('Vous n\'avez pas encore ajouté d\'adresse à votre compte.') }}
</p>
</div>
<x-button
label="{{ __('Créer ma première adresse') }}"
icon="o-plus"
link="{{ route('addresses.create') }}"
class="btn-primary btn-sm mt-2"
/>
</div>
</x-card>
@endif
</div>
Et voilà l'aspect :

En prévoyant le cas où il n'y a pas encore d'adresse, ce qui est la situation lorsque le client vient de s'inscrire :

La facturation électronique
Toutes les entreprises françaises, quelle que soit leur taille, devront émettre des factures électroniques à la date du 01/09/2027. Alors, il faut s'y préparer ! Un annuaire officiel recense les entreprises et entités publiques assujetties aux obligations d’émettre et de recevoir des factures électroniques. L'élément d'entrée dans cet annuaire est le SIREN ou SIRET. Vous pouvez trouver la documentation officielle ici. Il faudra prévoir cette mention sur les factures. Mais on ne doit pas rendre cette saisie obligatoire avant cette date. On prévoit cette date dans le fichier .env :
FACTURATION_ELECTRONIQUE_OBLIGATOIRE_DATE=2027-09-01
Et dans le fichier de configuration de l'application (app.php) pour récupérer la valeur :
'facturation_electronique' => [
'obligatoire_a_partir_du' => env('FACTURATION_ELECTRONIQUE_OBLIGATOIRE_DATE', '2027-09-01'),
],
On va utiliser cette valeur pour déterminer si on doit faire apparaître ou pas cette saisie dans les adresses. Il faudra aussi tenir compte après cette date des adresses déjà saisies sans cette valeur pour une nouvelle commande, mais on verra ça plus tard...
Un formulaire pour les adresses
On va avoir un formulaire commun pour la création et la modification. On ajoute un composant Blade pour ce formulaire :
php artisan make:component addresses.form --view

Avec ce code :
<x-form wire:submit="save" class="w-full space-y-4">
{{-- Adresse Pro --}}
<x-checkbox
label="{!! __('C\'est une adresse professionnelle') !!}"
wire:model="professionnal"
wire:change="$refresh"
class="checkbox-primary checkbox-sm"
/>
{{-- Civilités --}}
<x-radio
:options="$civilities"
wire:model="civility"
class="radio-primary radio-sm"
/>
{{-- Nom & Prénom sur 2 colonnes en Desktop --}}
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<x-input
label="{{ __('Nom') }}"
type="text"
wire:model="name"
icon="o-user"
hint="{{ $professionnal ? __('Optionnel') : '' }}"
:required="!$professionnal"
/>
<x-input
label="{{ __('Prénom') }}"
type="text"
wire:model="firstname"
icon="o-user"
hint="{{ $professionnal ? __('Optionnel') : '' }}"
:required="!$professionnal"
/>
</div>
{{-- Raison Sociale --}}
<x-input
label="{{ __('Raison sociale') }}"
type="text"
wire:model="company"
icon="o-building-office"
:disabled="!$professionnal"
:required="$professionnal"
/>
<x-input
label="{{ __('Numéro de TVA') }}"
type="text"
wire:model="buyer_tax_no"
icon="o-identification"
:disabled="!$professionnal"
hint="A compléter si vous êtes redevable de la TVA. Example: FR12345678901"
/>
@php
$dateObligation = \Carbon\Carbon::parse(config('app.facturation_electronique.obligatoire_a_partir_du'));
$sirenObligatoire = $professionnal && ($country?->requires_siren ?? false) && $dateObligation->isPast();
$dateFormatee = $dateObligation->translatedFormat('d/m/Y');
@endphp
<x-input
label="{{ __('Numéro de SIREN/SIRET') }}"
type="text"
wire:model="siren"
icon="o-identification"
:disabled="!$professionnal || !$country?->requires_siren || !$sirenObligatoire"
:required="$sirenObligatoire"
hint="{{ $sirenObligatoire
? __('Obligatoire pour la facturation électronique. Exemple: 123456789 (SIREN) ou 12345678901234 (SIRET)')
: ($professionnal && $country?->requires_siren
? __('Ce champ sera obligatoire à partir du :date pour la facturation électronique.', ['date' => $dateFormatee])
: '') }}"
/>
{{-- Lignes d'adresse --}}
<x-input
label="{{ __('N° et nom de rue') }}"
type="text"
wire:model="address"
icon="o-map-pin"
required
/>
<x-input
label="{{ __('Bâtiment, étage, etc.') }}"
type="text"
hint="{{ __('Optionnel') }}"
wire:model="addressbis"
icon="o-map-pin"
/>
<x-input
label="{{ __('Lieu-dit ou Boîte Postale') }}"
type="text"
hint="{{ __('Optionnel') }}"
wire:model="bp"
icon="o-map-pin"
/>
{{-- Code Postal & Ville sur 2 colonnes --}}
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4">
<x-input
label="{{ __('Code postal') }}"
type="text"
wire:model="postal"
icon="o-map-pin"
required
/>
<x-input
label="{{ __('Ville') }}"
type="text"
wire:model="city"
icon="o-map-pin"
class="sm:col-span-2"
required
/>
</div>
{{-- Pays & Téléphone --}}
<x-select
label="{{ __('Pays') }}"
:options="$countries"
wire:model.live.debounce="country_id"
icon="o-globe-alt"
required
/>
<x-input
label="{{ __('Numéro de téléphone') }}"
type="tel"
wire:model="phone"
icon="o-phone"
required
/>
<p class="text-xs text-base-content/60 pt-2">
<span class="text-error">*</span> @lang('Champs obligatoires')
</p>
<x-slot:actions>
<div class="flex flex-col sm:flex-row items-center justify-between gap-3 w-full pt-2">
<x-button
label="{{ __('Annuler') }}"
type="button"
icon="o-x-mark"
link="/account/addresses"
class="btn-ghost btn-sm w-full sm:w-auto order-2 sm:order-1"
/>
<x-button
label="{{ __('Enregistrer') }}"
type="submit"
icon-right="o-check"
spinner="save"
class="btn-primary btn-sm w-full sm:w-auto order-1 sm:order-2"
/>
</div>
</x-slot:actions>
</x-form>
Un trait pour les éléments communs
De la même manière, on va avoir des éléments PHP communs pour la création et la modification d'une adresse, on crée un trait :

On y place les propriétés, les règles de validation et deux fonctions :
<?php
namespace App\Traits;
use App\Models\Country;
use Illuminate\Support\Collection;
use Carbon\Carbon;
trait ManageAddress
{
public Collection $countries;
public bool $professionnal = false;
public array $civilities = [['id' => 'M.', 'name' => 'M.'], ['id' => 'Mme', 'name' => 'Mme']];
public string $civility = 'M.';
public ?string $firstname = null;
public ?string $name = null;
public ?string $company = null;
public ?string $address = null;
public ?string $buyer_tax_no = null;
public ?string $siren = null;
public ?string $addressbis = null;
public ?string $bp = null;
public string $postal = '';
public string $city = '';
public ?Country $country = null;
public int $country_id;
public string $phone = '';
/**
* @return array
*/
protected function rules(): array
{
$sirenObligatoire = $this->professionnal
&& $this->country?->requires_siren
&& Carbon::parse(config('app.facturation_electronique.obligatoire_a_partir_du'))->isPast();
return [
'professionnal' => 'required|boolean',
'civility' => 'required|in:M.,Mme',
'firstname' => 'required_unless:professionnal,true|nullable|string|max:100',
'name' => 'required_unless:professionnal,true|nullable|string|max:100',
'company' => 'required_unless:professionnal,false|nullable|string|max:100',
'address' => 'required|string|max:255',
'addressbis' => 'nullable|string|max:255',
'bp' => 'nullable|string|max:100',
'postal' => 'required|string',
'city' => 'required|string|max:100',
'country_id' => 'required|integer|exists:countries,id',
'phone' => 'required|numeric',
'buyer_tax_no' => [
'nullable',
'string',
'max:20',
function ($attribute, $value, $fail) {
// Si le client est un professionnel ET que buyer_tax_no est renseigné
if ($this->professionnal && $value !== null) {
// Regex pour les numéros de TVA intracommunautaire (format UE)
$tvaUeRegex = '/^[A-Z]{2}[0-9]{8,12}$/'; // Ex: FR12345678901, DE123456789
if (!preg_match($tvaUeRegex, $value)) {
$fail('Le numéro de TVA semble invalide. Format attendu : [PAYS][CHIFFRES] (ex: FR12345678901).');
}
}
},
],
'siren' => [
'nullable',
'string',
'max:14',
function ($attribute, $value, $fail) use ($sirenObligatoire) {
if ($sirenObligatoire) {
if (empty($value)) {
$fail('Le SIREN ou SIRET est obligatoire pour la facturation électronique.');
return;
}
$sirenRegex = '/^[0-9]{9}$/'; // SIREN
$siretRegex = '/^[0-9]{14}$/'; // SIRET
if (!preg_match($sirenRegex, $value) && !preg_match($siretRegex, $value)) {
$fail('Le SIREN ou SIRET doit contenir 9 ou 14 chiffres.');
}
}
},
],
];
}
/**
* Met à jour les propriétés en fonction des changements de champs.
*
* @param string $property Le nom de la propriété modifiée.
* @param mixed $value La nouvelle valeur de la propriété.
*/
public function updated(string $property, mixed $value): void
{
match ($property) {
'country_id' => $this->country = Country::find($value),
'professionnal' => $this->resetProfessionalFields(),
default => null,
};
}
/**
* @return void
*/
private function resetProfessionalFields(): void
{
$this->company = null;
$this->buyer_tax_no = null;
$this->siren = null;
}
}
La création d'une adresse
On crée le composant Livewire pour la création d'une adresse :
php artisan make:livewire pages::account.addresses.create

Avec la création précédemment du composant pour le formulaire et le trait, le code est bien plus léger :
<?php
use App\Models\Country;
use Livewire\Component;
use Livewire\Attributes\Title;
use Mary\Traits\Toast;
use Illuminate\Support\Collection;
use App\Traits\ManageAddress;
use Illuminate\Support\Facades\Auth;
new #[Title('Créer une adresse')]
class extends Component {
use Toast, ManageAddress;
public function mount(): void
{
$this->countries = Country::all();
if ($this->countries->isNotEmpty()) {
$this->country_id = $this->countries->first()->id;
$this->country = $this->countries->first(); // Charge le pays par défaut
}
}
public function save(): void
{
$data = $this->validate($this->rules());
Auth::user()->addresses()->create($data);
$this->success(__('Adresse créée avec succès.'), redirectTo: route('addresses'));
}
}
?>
<div class="max-w-3xl mx-auto py-8 px-4 space-y-6">
{{-- En-tête avec bouton de retour --}}
<x-header title="{{ __('Créer une adresse') }}" subtitle="{{ __('Renseignez vos coordonnées de livraison ou de facturation') }}" separator>
<x-slot:actions>
<x-button
label="{{ __('Retour aux adresses') }}"
icon="o-arrow-left"
link="{{ route('addresses') }}"
class="btn-ghost btn-sm"
/>
</x-slot:actions>
</x-header>
{{-- Carte du formulaire --}}
<x-card class="shadow-xl bg-base-100" separator progress-indicator>
<x-addresses.form
:civilities="$civilities"
:countries="$countries"
:professionnal="$professionnal"
:country="$country"
/>
</x-card>
</div>
On obtient ce gros formulaire :


Le nom et le prénom sont optionnels si on a affaire à une adresse professionnelle. Vérifiez que tout fonctionne correctement.
Le SIREN/SIRET est obligatoire si le pays est concerné (France Métropolitaine et territoires d'outre-mer) dans le cadre de la facturation électronique à partir de la date du 01/09/2027. En attendant, on affiche :

Mais après cette date pour une adresse localisée en France, on affichera :

La modification d'une adresse
On crée le composant pour la modification d'une adresse :
php artisan make:livewire pages::account.addresses.edit

Là aussi le code est léger :
<?php
use App\Models\{Address, Country};
use Livewire\Component;
use Livewire\Attributes\Title;
use Mary\Traits\Toast;
use Illuminate\Support\Collection;
use App\Traits\ManageAddress;
use Illuminate\Support\Facades\Auth;
new #[Title('Modifier l\'adresse')]
class extends Component {
use Toast, ManageAddress;
public Address $myAddress;
public function mount(Address $address): void
{
if ($address->user_id !== Auth::id()) {
abort(403);
}
$this->myAddress = $address;
$this->fill($this->myAddress);
$this->countries = Country::all();
$this->country = $this->countries->find($this->country_id); // ✅ Charge le pays correspondant à l'adresse
}
public function save(): void
{
$data = $this->validate($this->rules());
$this->myAddress->update($data);
$this->success(__('Adresse mise à jour avec succès.'), redirectTo: route('addresses'));
}
}; ?>
<div class="max-w-3xl mx-auto py-8 px-4 space-y-6">
{{-- En-tête de la page --}}
<x-header
title="{!! __('Modifier l\'adresse') !!}"
subtitle="{{ __('Mettez à jour vos informations de livraison ou de facturation') }}"
separator
>
<x-slot:actions>
<x-button
label="{{ __('Retour aux adresses') }}"
icon="o-arrow-left"
link="{{ route('addresses') }}"
class="btn-ghost btn-sm"
/>
</x-slot:actions>
</x-header>
{{-- Carte du formulaire --}}
<x-card class="shadow-xl bg-base-100 overflow-hidden" separator progress-indicator>
<x-addresses.form
:civilities="$civilities"
:countries="$countries"
:professionnal="$professionnal"
:country="$country"
/>
</x-card>
</div>
Le formulaire est évidemment identique, à la différence qu'il est maintenant renseigné avec l'adresse existante au chargement.
Conclusion
Les clients ont désormais accès à leurs données personnelles et peuvent même les modifier. D'autre part, ils disposent d'une gestion complète pour leurs adresses. La prochaine étape consistera à afficher les commandes passées et aussi de se plier aux exigences du RGPD.
Par bestmomo
Aucun commentaire