Hello, dans cette partie nous allons nous concentré sur la gestion de nos articles avec ElasticSearch. Comme prévu, nos articles seront stocker dans une Base de données un peu spécial. ElasticSearch, l’avantage de cet outil c’est qu’il s’agit d’un base NoSql. Basé sur un système de clé valeur, ces bases sont souvent très puissante quand il s’agit de stocker des données « non prévisible », en effet, il n’y a pas besoin de définir un schéma lorsque l’on stocke la donnée. De plus ElasticSearch est un très bon moteur de recherche il nous permettra d’avoir des résultats pertinents.

Les dépendances

Une fois votre base ES mise en place ( voir documentation officielle ). Vous aller devoir installer une dépendances grâce a composer. Elle permettra d’interagir avec ES avec PHP sans difficulté. Pour ma part, ES est installé en tant que service, il sera donc accessible en localhost sur le port par défaut 9200. Il faut savoir qu’il existe de nombreux package laravel permettant d’ajouter une couche de gestion d’ES directement, ou encore d’ajouter ES à Eloquent. Dans mon cas j’ai choisit de vous présentez le package officiel d’ES. Ce qui veut donc dire, que vous pourrez dans n’importe quel projet php ajouter ce package et vous en servir de la même façon. Je vous joint également la documentation lié au package .

composer require elasticsearch/elasticsearch

Création d’un article

Il faut maintenant créer, comme a chaque fois, nos méthodes et nos vues. Pour les routes nous utiliserons resource pour ne pas perdre de temps. Mais avant tout autre action créons notre controller grâce a artisan.

php artisan make:controller ArticleController
#/routes/web.php

Route::resource('/articles','ArticleController');

De la même façon il faudra créer un index ES pouvant stocker nos articles. Nous l’appellerons simplement article. Pour faire simple, nous allons créer une commande

php artisan make:command CreateIndices

Voici son contenu

protected $signature = 'elastic:indices';
protected $description = 'Création des index';

public function handle()
{
        $build = ClientBuilder::create();
        $build->setHosts(['localhost']);
        $client = $build->build();
        $params = [
            'index' => 'articles',
            'body' => [
                'settings' => [
                    'number_of_shards' => 2,
                    'number_of_replicas' => 1
                ],
                'mappings' => [
                    'data' => [
                        '_source' => [
                            'enabled' => true
                        ],
                        'properties' => [
                            'titre' => [
                                'type' => 'text',
                            ],
                            'slug' => [
                                'type' => 'keyword'
                            ],
                            'resume' => [
                                'type' => 'text'
                            ],
                            'content' => [
                                'type' => 'text'
                            ],
                            'categorie_id' => [
                                'type' => 'integer'
                            ],
                            'categorie_name' => [
                                'type' => 'text'
                            ],
                            'categorie_slug' => [
                                'type' => 'text'
                            ],
                            'couverture' => [
                                'type' => 'text'
                            ],
                            'statut' => [
                                'type' => 'text'
                            ],
                            'created_at' => [
                                'type' => 'date'
                            ]
                        ]
                    ]
                ]
            ]
        ];

        $client->indices()->create($params);
}

L’etape suivante est simplement d’appeler votre nouvelle commande

php artisan elastic:indices

Et voila, en accédant à 127.0.0.1:9200/articles vous retrouverez votre mapping défini dans la commande. Passons au controller, j’ajoute également un constructeur me permettant d’ajouter le middleware d’authentification mais aussi de créer une connexion entre ES et Laravel.

#/app/Http/Controller/ArticleController.php
private $articles;
private $articles_index = 'articles';

public function __construct()
{
    $this->middleware('auth');
    $build = ClientBuilder::create();
    $build->setHosts(['localhost']);
    return $this->articles = $build->build();
}

Attention: n’oubliez pas d’ajouter le package au controller

use Elasticsearch\ClientBuilder;

On trouvera une petite différence dans notre méthode create, il faudra ajouter les categories pour les liés aux articles.

#/app/Http/Controller/ArticleController.php
public function create()
{
     $categories = Categorie::all();
     return view('articles.create', compact('categories'));
}

public function store(Request $request)
{
   $file = $request->file('couverture');
   $destinationPath = 'storage/uploads';
   $file->move($destinationPath, $file->getClientOriginalName());
   $categorie = Categorie::find($request->categorie);
   $data = [
       'titre' => $request->titre,
       'slug' => Str::slug($request->titre, '-'),
       'resume' => $request->resume,
       'content' => $request->content_article,
       'categorie_name' => $categorie->name,
       'categorie_slug' => $categorie->slug,
       'categorie_id' => $categorie->id,
       'couverture' => $destinationPath . '/' . $file->getClientOriginalName(),
       'statut' => $request->statut,
       'created_at' => strtotime('today')
      ];

        $params = [
            'index' => $this->articles_index,
            'type' => 'data',
            'body' => $data
        ];

        $result = $this->articles->index($params);
        sleep(1); // Le temps d'enregistrement de l'elastic
        return redirect()->route('articles.index');

    }

#/resources/views/articles/create.blade.php

@extends('layouts.app')

@section('content')
    <link href="http://cdnjs.cloudflare.com/ajax/libs/summernote/0.8.11/summernote.css" rel="stylesheet">
    <div class="container">
        <div class="row">
            <div class="col-6 offset-3">
                <form action="{{ route('articles.store') }}" method="post" enctype="multipart/form-data">
                    <div class="form-group">
                        <label for="titre">Titre</label>
                        <input type="text" name="titre" id="titre" class="form-control" required>
                    </div>
                    <div class="form-group">
                        <label for="resume">Resumé</label>
                        <textarea name="resume" id="resume" class="form-control"></textarea>
                    </div>
                    <div class="form-group">
                        <label for="content">Content</label>
                        <textarea name="content_article" id="content" class="form-control " ></textarea>
                    </div>
                    <div class="form-group">
                        <label for="categorie">Categories</label>
                        <select name="categorie" class="form-control" id="categorie">
                            @foreach($categories as $category)
                                <option value="{{ $category['id'] }}">{{ $category['name'] }}</option>
                                @endforeach
                        </select>
                    </div>
                    <div class="form-group">
                        <label for="couverture">Image de couverture</label>
                        <input type="file" id="couverture" name="couverture" class="form-control">
                    </div>
                    <div class="form-group">
                        <label for="statut">Statut</label>
                        <select name="statut" class="form-control" id="statut">
                            <option value="1">Publié</option>
                            <option value="2">Brouillon</option>
                        </select>
                    </div>
                    @csrf()
                    <input type="submit" class="btn btn-success">
                </form>
            </div>
        </div>
    </div>

@endsection

@section('script')
    <script src="https://cdn.ckeditor.com/4.11.3/standard/ckeditor.js"></script>

    <script>
        $(document).ready(function () {
            CKEDITOR.replace('content');
        })
    </script>
    @endsection

Une petite explication de la fonction store :

  • La première partie nous permet de gérer le cas des fichiers car nous devons stocker une image de couverture.
  • La variable data contient toutes les informations que ES va devoir ingérer.
  • $params est le tableau de paramètre dont la librairie a besoin pour faire comprendre a Elastic qu’il faut intégré ces données a un endroit précis.
  • Sleep(1) , cela permet simplement d’attendre une seconde pour que ES stocke la donnée proprement et nous redirige ensuite vers la page de liste que nous créerons plus tard.

Modification des articles

Controller et vues sont de sortie comme toujours.

#/app/Http/Controller/ArticleController

public function edit($article)
{
    $params = [
        'index' => $this->articles_index,
        'type' => 'data',
        'id' => $article
    ];
    $data = $this->articles->get($params);
    $categories = Categorie::all();
    #Le statut peut être dynamique et devenir une resource en 
    #base de données
    $statut = [
        ['id' => 1, 'value' => 'publié'],
        ['id' => 2, 'value' => 'brouillon']
    ];
    if ($data) {
        return view('articles.edit', ['article' => $data, 'categories' => $categories, 'statut' => $statut]);
    }
}

 public function update(Request $request, $article)
    {
        $file = $request->file('couverture');
        $categorie = Categorie::find($request->categorie);
        $data = [
            'titre' => $request->titre,
            'slug' => Str::slug($request->titre, '-'),
            'resume' => $request->resume,
            'content' => $request->content_article,
            'categorie_name' => $categorie->name,
            'categorie_slug' => $categorie->slug,
            'categorie_id' => $categorie->id,
            'statut' => $request->statut,
            'created_at' => strtotime('today')
        ];

        if (!empty($file)) {
            $file = $request->file('couverture');
            $destinationPath = 'storage/uploads';
            $file->move($destinationPath, $file->getClientOriginalName());
            $data['couverture'] = $destinationPath . '/' . $file->getClientOriginalName();
        }


        $params = [
            'index' => $this->articles_index,
            'type' => 'data',
            'id' => $article,
            'body' => [
                'doc' => $data
            ]
        ];
        $this->articles->update($params);
        sleep(1);
        return redirect()->route('articles.index');
    }

#/resources/views/articles/edit.blade.php
@extends('layouts.app')

@section('content')
    <link href="http://cdnjs.cloudflare.com/ajax/libs/summernote/0.8.11/summernote.css" rel="stylesheet">
    <div class="container">
        <div class="row">
            <div class="col-6 offset-3">
                <form action="{{ route('articles.update',['article' => $article['_id']]) }}" method="post" enctype="multipart/form-data">
                    <div class="form-group">
                        <label for="titre">Titre</label>
                        <input type="text" name="titre" id="titre" class="form-control"
                               value="{{ $article['_source']['titre'] }}" required>
                    </div>
                    <div class="form-group">
                        <label for="resume">Resumé</label>
                        <textarea name="resume" id="resume"
                                  class="form-control">{{ $article['_source']['resume'] }}</textarea>
                    </div>
                    <div class="form-group">
                        <label for="content">Content</label>
                        <textarea name="content_article" id="content"
                                  class="form-control ">{{ $article['_source']['content'] }}</textarea>
                    </div>
                    <div class="form-group">
                        <label for="categorie">Categories</label>
                        <select name="categorie" class="form-control" id="categorie">
                            @foreach($categories as $category)
                                @if($article['_source']['categorie_id'] == $category['id'])
                                    <option value="{{ $category['id'] }}" selected>{{ $category['name'] }}</option>
                                @else
                                    <option value="{{ $category['id'] }}">{{ $category['name'] }}</option>
                                @endif
                            @endforeach
                        </select>
                    </div>
                    <div class="form-group">
                        <label for="couverture">Image de couverture</label>
                        <input type="file" id="couverture" name="couverture" class="form-control">
                    </div>
                    <div class="form-group">
                        <label for="statut">Statut</label>
                        <select name="statut" class="form-control" id="statut">
                            @foreach($statut as $s)
                                @if($s['id'] == $article['_source']['statut'])
                                    <option value="{{ $s['id'] }}" selected>{{ $s['value'] }}</option>
                                @else
                                    <option value="{{ $s['id'] }}">{{ $s['value'] }}</option>
                                @endif
                            @endforeach
                        </select>
                    </div>
                    @csrf()
                    @method('PUT')
                    <input type="submit" class="btn btn-success">
                </form>
            </div>
        </div>
    </div>

@endsection

@section('script')
    <script src="https://cdn.ckeditor.com/4.11.3/standard/ckeditor.js"></script>

    <script>
        $(document).ready(function () {
            CKEDITOR.replace('content');
        })
    </script>
@endsection

Et voila , rien de bien difficile dans notre update . On a simplement modifier la structure de $params et la méthode appelé pour modifier au lieu de créer. Pour terminer la suppression d’un article.

Suppression d’un article

#/app/Http/Controller/ArticleController.php
 public function destroy($article)
    {
        $params = [
            'index' => $this->articles_index,
            'type' => 'data',
            'id' => $article
        ];
        $this->articles->delete($params);
        sleep(1);
        return redirect()->route('articles.index');
    }

Une suppression simple grâce à la méthode elasticsearch-php.

Liste des articles

Pour terminer, il faut une page de liste des articles. Le fonctionnement va très peu changer. La différence sera la requête de récupération. Nous n’utiliserons pas Eloquent mais bien une simple requête ES.

#/app/Http/Controller/ArticleController.php
public function index()
    {
        $params = [
            'index' => $this->articles_index,
            'type' => 'data'
        ];

        $result = $this->articles->search($params);

        $articles = !empty($result['hits']['hits']) ? $result['hits']['hits'] : [];
        $number = !empty($result['hits']['total']) ? $result['hits']['total'] : 0;

        return view('articles.index', ['articles' => $articles, 'number_article' => $number]);
    }

#/resources/views/articles/index.blade.php

@extends('layouts.app')

@section('content')
    <div class="container">
        <div class="row">
            <div class="col-2 offset-10">
                <a href="{{ route('articles.create') }}" class="btn btn-info">Création d'un article</a>
            </div>
        </div>
        <div class="row">
            <div class="col-8 offset-2">
                <table class="table">
                    <thead>
                    <tr>
                        <th>Nom</th>
                        <th>Slug</th>
                        <th>Crée le</th>
                        <th>Actions</th>
                    </tr>
                    </thead>
                    <tbody>
                    @foreach($articles as $article)
                        <tr>
                            <td>{{ $article['_source']['titre'] }}</td>
                            <td>{{ $article['_source']['slug'] }}</td>
                            <td>{{ date('d-m-Y h:m',$article['_source']['created_at'])  }}</td>
                            <td>
                                <div class="row">
                                    <div class="col">
                                        <a href="{{ route('articles.edit',['article' => $article['_id']]) }}"
                                           class="btn btn-sm btn-warning">Modifier</a>
                                    </div>
                                    <div class="col">
                                        <form action="{{ route('articles.destroy',['article' => $article['_id']]) }}" method="post">
                                            <input type="submit" class="btn btn-danger btn-sm" value="Supprimer">
                                            @csrf()
                                            @method('DELETE')
                                        </form>
                                    </div>
                                </div>

                            </td>
                        </tr>
                    @endforeach
                    </tbody>

                </table>
            </div>
        </div>
    </div>
@endsection

C’est terminé, nous avons construit la gestion des articles et nous pouvons donc créer , modifier ou encore supprimer notre ressource. N’hésitez pas a adapter ce concept assez simple a vos types de contenu : sur vos pages, vos produits ou autre. N’hésitez pas à poser des questions ou a me faire des retours sur votre propre code. En attendant la prochaine étape, je vous souhaite un bon code.

A bientôt

Show CommentsClose Comments

Leave a comment