Laravel 5

Un site d’annonces – les données

Je vous propose un nouveau projet avec Laravel 5.8 consacré aux petites annonces. Il en existe de très nombreux sur la toile dont le plus célèbre est certainement leboncoin. Le but n’est pas d’en faire un clone mais de s’inspirer de sa philosophie pour créer une application simple et efficace.

Le but est de faire découvrir le framework Laravel à ceux qui ne le connaissent pas encore et de proposer un cas pratique pour ceux qui veulent le voir en action.

Au niveau des fonctionnalités attendues :

  • une interface visuelle pour sélectionner la région
  • une sélection progressive par département et commune
  • la possibilité de déposer une annonce sans créer de compte
  • avoir des catégories de produits et propositions
  • choisir le nombre de semaines de parution
  • avoir un système de modération pour les annonces et les messages
  • pouvoir ajouter des photos aux annonces avec une interface visuelle
  • proposer un compte avec gestion des annonces déposées
  • avoir une administration pour gérer les modérations et les annonces obsolètes
  • on va faire une application strictement française et donc ne pas prévoir le changement de langage

Dans ce premier article on va mettre en place le schéma des données qui constitue le socle de l’application.

Pour vous simplifier la vie vous pouvez télécharger le dossier complet pour le code de cet article. Vous y trouverez en particulier le fichier de population trop long pour figurer dans l’article, ainsi que les images d’exemple qui serviront pour le site.

Installation de Laravel

Dans un premier temps on va installer la dernière version de Laravel :

composer create-project --prefer-dist laravel/laravel mesannonces

On attend que tout se crée et se mette en place…

Si vous obtenez la page d’accueil c’est parfait :On va aussi créer tout ce qu’il faut pour l’authentification :

php artisan make:auth

Créez aussi une base de données qu’on va appeler annonces pour rester simples.

N’oubliez pas de renseigner correctement le fichier .env pour votre base, par exemple :

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=annonces
DB_USERNAME=root
DB_PASSWORD=

Les données utilisateurs

Par défaut Laravel arrive avec ces deux migrations :

Pour la table users on a par défaut :

Schema::create('users', function (Blueprint $table) {
    $table->bigIncrements('id');
    $table->string('name');
    $table->string('email')->unique();
    $table->timestamp('email_verified_at')->nullable();
    $table->string('password');
    $table->rememberToken();
    $table->timestamps();
});

On va ajouter une colonne pour distinguer les administrateurs des utilisateurs de base :

$table->boolean('admin')->default(false);

Par défaut on a false, donc chaque fois que quelqu’un va s’inscrire il sera simple utilisateur sans qu’on ait à s’en soucier.

La deuxième table concerne la réinitialisation du mot de passe et n’a pas besoin de modification pour notre application.

Pour le moment on ne va pas ajouter la vérification de l’email, on verra plus tard si on l’ajoute.

Les régions

On va créer une table pour disposer des régions françaises :

php artisan make:migration create_regions_table --create=regions

Par défaut on a la clé primaire et les données temporelles :

Schema::create('regions', function (Blueprint $table) {
    $table->bigIncrements('id');
    $table->timestamps();
});

Pour les régions on va ajouter un nom et un slug :

$table->string('name')->unique();
$table->string('slug')->unique();

Le nom est pour l’affichage et le slug pour l’url.

On ne va pas se compliquer la vie en créant également une table pour les départements parce qu’ils sont nombreux, et encore moins pour les communes ! Alors comment procéder ?

Comme on a de la chance il existe une API officielle pour obtenir ces informations avec facilité :

Si vous parcourez cette API vous verrez que les régions sont identifiées avec un code, par exemple pour la Bretagne c’est 53 et pour l’Occitanie 76.

Pour pouvoir utiliser cette API pour récupérer les départements on va ajouter les codes dans notre table des régions :

$table->tinyInteger('code')->unique();

En résumé notre table regions aura ces colonnes :

Schema::create('regions', function (Blueprint $table) {
    $table->bigIncrements('id');
    $table->string('name')->unique();
    $table->string('slug')->unique();
    $table->tinyInteger('code')->unique();
    $table->timestamps();
});

Les catégories

On crée aussi la migration pour les catégories d’annonces :

php artisan make:migration create_categories_table --create=categories

La aussi on se retrouve avec la clé primaire et les données temporelles.

On va ajouter juste un nom :

Schema::create('categories', function (Blueprint $table) {
    $table->bigIncrements('id');
    $table->string('name')->unique();
    $table->timestamps();
});

Les annonces

Les annonces sont le cœur de notre application, alors on crée aussi une table :

php artisan make:migration create_ads_table --create=ads

On va avoir besoin de plus de colonnes dans cette table, faisons le point :

  • title : un titre pour l’annonce
  • texte : un texte pour l’annonce
  • category_id : une clé étrangère pour la catégorie
  • region_id : une clé étrangère pour la région
  • user_id : une clé étrangère pour l’utilisateur
  • departement : le code du département
  • commune : le code de la commune
  • : le nom de la commune
  • commune_postal : le code postal de la commune
  • pseudo : le pseudonyme de celui qui dépose l’annonce
  • email : l’e pseudonyme de celui qui dépose l’annonce
  • limit : la date limite de la publication
  • active : l’état actif de l’annonce (si elle a été acceptée)

Ça nous donne ce code :

Schema::create('ads', function (Blueprint $table) {
    $table->bigIncrements('id');
    $table->string('title');
    $table->text('texte');
    $table->unsignedBigInteger('category_id');
    $table->unsignedBigInteger('region_id');
    $table->unsignedBigInteger('user_id')->default(0);
    $table->string('departement');
    $table->string('commune');
    $table->string('commune_name');
    $table->string('commune_postal');
    $table->string('pseudo');
    $table->string('email');
    $table->date('limit');
    $table->boolean('active')->default(false);
    $table->timestamps();

    $table->foreign('category_id')->references('id')->on('categories');
    $table->foreign('region_id')->references('id')->on('regions');
});

On ne crée pas de contrainte pour la clé étrangère de l’utilisateur parce qu’une annonce n’est pas forcément déposée pour un utilisateur connecté et dans ce cas on a la valeur 0 et aucune relation avec la table users.

Les messages

Si un utilisateur connecté envoie un message il est transmis immédiatement à l’email déclaré dans l’annonce. par contre un message d’un utilisateur non connecté est mémorisé dans la table et est modéré par un administrateur.

On crée donc une table pour les messages :

php artisan make:migration create_messages_table --create=messages

Là on aura besoin d’un texte, d’un email et d’une clé étrangère pour savoir à quelle annonce il est fait référence :

Schema::create('messages', function (Blueprint $table) {
    $table->bigIncrements('id');
    $table->text('texte');
    $table->string('email');
    $table->unsignedBigInteger('ad_id');
    $table->timestamps();

    $table->foreign('ad_id')->references('id')->on('ads')->onDelete('cascade');
});

On crée une suppression en cascade pour avoir l’élimination des messages pour une annonce supprimée.

Les photos

Pour le téléchargement des photos on va faire quelque chose de dynamique et qui dit dynamique dit mémorisation des données. On crée donc une table pour les téléchargements :

php artisan make:migration create_uploads_table --create=uploads

Avec ce code :

Schema::create('uploads', function (Blueprint $table) {
    $table->bigIncrements('id');
    $table->string('filename');
    $table->string('original_name');
    $table->string('index');
    $table->unsignedBigInteger('ad_id');
    $table->timestamps();
});

Je commenterai l’intérêt de ces colonnes lorsque je parlerai de cette partie de l’application.

La migration

Vous pouvez maintenant lancer la migration pour voir si tout se passe correctement :

php artisan migrate

Vous devez vous retrouver avec vos 8 tables dans la base :

Si ce n’est pas le cas revoyez votre code pour trouver l’erreur…

Les modèles

User

Comme on va utiliser Eloquent on va avoir besoin de modèles pour nos tables. Par défaut on a déjà le mode User ici :

Pour des raison d’organisation on va créer un dossier app\Models et y transporter User :

Pour pas avoir de souci on va mettre à jour l’espace de nom dans User :

namespace App\Models;

Il faut aussi actualiser le code dans config\auth.php :

'providers' => [
    'users' => [
        'driver' => 'eloquent',
        'model' => App\Models\User::class,
    ],

Et pour être complet on va mettre à jour app\Http\Controllers\Auth\RegisterController :

use App\Models\User;

Enfin on doit établir la relation avec les annonces dans le modèle (un utilisateur peut avoir plusieurs annonces) :

/**
 * Get the ads for the user.
 */
public function ads()
{
    return $this->hasMany(Ad::class);
}

Category et Upload

Pour ces modèles on va faire simple :

php artisan make:model App\Models\Category
php artisan make:model App\Models\Upload

On se contentera du code par défaut.

Message

On utilise aussi Artisan pour Message :

php artisan make:model App\Models\Message

Mais on va compléter le code pour pouvoir utiliser la méthode create. D’autre part on a une relation avec les annonces parce qu’un message appartient (belongsTo) à une annonce :

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Message extends Model
{
    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [
        'texte',
        'email',
        'ad_id',
    ];

    /**
     * Get the ad that owns the message.
     */
    public function ad()
    {
        return $this->belongsTo(Ad::class);
    }
}

Region

On utilise aussi Artisan pour Region :

php artisan make:model App\Models\Region

Ici on a une relation avec les annonces parce qu’une région a plusieurs (hasMany) annonces :

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Region extends Model
{
    /**
     * Get the ads for the region.
     */
    public function ads()
    {
        return $this->hasMany(Ad::class);
    }
}

Ad

On crée aussi le modèle Ad pour les annonces :

php artisan make:model App\Models\Ad

On va ajouter la propriété $fillable pour pouvoir utiliser create. Et on établit les relations :

  • appartient (belongsTo) une région
  • appartient (belongsTo) une catégorie
  • a plusieurs (hasMany) photos (Upload)

Ce qui donne ce code :

<?php

namespace App\Models;

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

class Ad extends Model
{
    use Notifiable;

    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [
        'title',
        'texte',
        'category_id',
        'region_id',
        'user_id',
        'departement',
        'commune',
        'commune_name',
        'commune_postal',
        'pseudo',
        'email',
        'limit',
        'active',
    ];

    /**
     * Get the region that owns the ad.
     */
    public function region()
    {
        return $this->belongsTo(Region::class);
    }

    /**
     * Get the category that owns the ad.
     */
    public function category()
    {
        return $this->belongsTo(Category::class);
    }

    /**
     * Get the photos for the ad.
     */
    public function photos()
    {
        return $this->hasMany(Upload::class);
    }
}

Le schéma

Voici une illustration du schéma des données avec les relations établies :

On ne retrouve pas les relations pour lesquelles on a pas établi de contrainte dans la base. Par exemple entre les users et les annonces (ads). Il en est de même entre les uploads et les annonces.

Les factories

Pour nos tests on va avoir besoin de données. laravel propose des factories pour simplifier le remplissage des tables :

Par défaut on a juste une classe prévue pour les utilisateurs.

On va en ajouter un pour les annonces et un autre pour les messages.

Les messages

L’écriture d’un factory est simple, on crée un nouveau fichier PHP :

Avec ce code :

<?php

use Faker\Generator as Faker;

$factory->define(App\Models\Message::class, function (Faker $faker) {

    return [
        'email' => $faker->email,
        'texte' => $faker->paragraph($nbSentences = 3, $variableNbSentences = true),
        'ad_id' => rand(1, 10),
    ];
});

La bibliothèque faker est bien pratique pour crée des données aléatoires. Ici on génère un email et un paragraphe.

Les annonces

De la même manière on crée un fichier pour le factory des annonces :

Avec ce code :

<?php

use Faker\Generator as Faker;
use Carbon\Carbon;
use App\Models\Region;

function getRandGeo($url)
{
    $results = file_get_contents($url);
    $elements = json_decode($results, true);
    $id = rand(0, count($elements) - 1);
    return $elements[$id];
}

$factory->define(App\Models\Ad::class, function (Faker $faker) {

    $region_id = rand(1, 12);
    $region_code = Region::find($region_id)->code;

    $departement = getRandGeo('https://geo.api.gouv.fr/regions/' . $region_code . '/departements');
    $commune = getRandGeo('https://geo.api.gouv.fr/departements/' . $departement['code'] . '/communes');

    $obsolete = rand(0, 1);

    return [
        'title' => $faker->sentence($nbWords = 3, $variableNbWords = true),
        'texte' => $faker->paragraph($nbSentences = 3, $variableNbSentences = true),
        'category_id' => rand(1, 10),
        'region_id' => $region_id,
        'user_id' => rand(2, 3),
        'departement' => $departement['code'],
        'commune' => $commune['code'],
        'commune_name' => $commune['nom'],
        'commune_postal' => $commune['codesPostaux'][0],
        'pseudo' => $faker->word,
        'email' => $faker->email,
        'limit' => $obsolete ? Carbon::now()->subDayss(rand(1, 30)) : Carbon::now()->addWeeks(rand(1, 4)),
        'active' => rand(0, 1),
    ];
});

ici quelques commentaires s’imposent pour l’utilisation de l’API geo. On a une routine :

function getRandGeo($url)
{
    $results = file_get_contents($url);
    $elements = json_decode($results, true);
    $id = rand(0, count($elements) - 1);
    return $elements[$id];
}

On envoie un url, par exemple on veut un département aléatoire pour une région. Il faut utiliser l’url de la forme /regions/[codeRegion]/departements. Si on veut les départements de Bretagne on utilise https://geo.api.gouv.fr/regions/53/departements. La routine permet de trouver des départements et des communes de façon aléatoire.

La population

Il ne nous reste plus qu’à créer des données pour nos tables. On va utiliser le seeder :

Comme le code est un peu long je vous suggère de le récupérer dans le dossier à télécharger.

On crée :

  • 3 utilisateurs dont un administrateur
  • les régions
  • 10 catégories
  • 40 annonces
  • 10 messages
  • les uploads pour les images que vous trouvez dans le fichier à télécharger (dossiers publics images et thumbs). On verra cet aspect dans un prochain article

Par exemple je retrouve mes 3 users :

Mes 10 catégories :

Pour les annonces j’ai bien des départements et des communes aléatoires :

La langue

Pour terminer on va aller chercher les fichiers pour le français ici. Mettez le dossier du français ici :

Définissez ensuite le français comme langue par défaut dans config\app.php :

'locale' => 'fr',

Normalement les messages doivent maintenant apparaître en français :

Conclusion

On a maintenant une bonne organisation de nos tables, les modèles avec leurs relations et des données pour tester nos fonctionnalités ! Dans le prochain article on va pouvoir commencer à coder du visuel pour notre site d’annonces.

Print Friendly, PDF & Email

11 commentaires

Laisser un commentaire