J’ai souvent évoqué le site Laravel Schema Designer dans ce blog, notamment dans mon cours sur Laravel 5.3. Mais ce n’est pas le seul générateur même si sa principale qualité est d’offrir une interface graphique, ce qui le rend pour le moment unique. Dans cet article je vous propose de découvrir un autre générateur intéressant : celui d’infyOm.

Installation

La procédure d’installation se trouve sur cette page de la documentation.

La version rapide

La façon la plus simple d’installer le générateur est de partir de l’installation de base de Laravel qui comporte déjà les packages en question. D’autre part il est prévu le template back-office AdminLTE :

C’est certainement le template le plus utilisé actuellement et il faut avouer qu’il est vraiment bien équipé ! Mais ce n’est pas le sujet de cet article…

Il vous faudra quand même faire une installation avec composer (par sécurité faites aussi un update dans la foulée !), créer un fichier .env et générer une clé.

La version laborieuse

Si on veut ajouter le générateur sur une installation existante (Laravel 5.3) alors on doit passer par la voie classique dans composer.json :

"require": {
 "infyomlabs/laravel-generator": "5.3.x-dev",
 "laravelcollective/html": "^5.3.0",
 "infyomlabs/adminlte-templates": "5.3.x-dev"
}

Si on veut créer une API et profiter des annotations Swagger alors il faut aussi installer ces packages :

"require": {
 "infyomlabs/swagger-generator": "dev-master",
 "jlapp/swaggervel": "dev-master"
}

Ce qui permet de générer automatiquement la documentation :

Une autre possibilité consiste à pouvoir utiliser le générateur avec une table existante, ce qui peut être bien pratique. Pour en profiter il faut installer ce package :

"require": {
 "doctrine/dbal": "~2.3"
}

Il faut ensuite mettre les dépendances à jour avec composer :

composer update

Il faut ensuite renseigner la configuration (config/app.php) :

Collective\Html\HtmlServiceProvider::class,
Laracasts\Flash\FlashServiceProvider::class,
Prettus\Repository\Providers\RepositoryServiceProvider::class,
\InfyOm\Generator\InfyOmGeneratorServiceProvider::class,
\InfyOm\AdminLTETemplates\AdminLTETemplatesServiceProvider::class, 

...

'Form' => Collective\Html\FormFacade::class,
'Html' => Collective\Html\HtmlFacade::class,
'Flash' => Laracasts\Flash\Flash::class,

Il y a ensuite des publications à effectuer

php artisan vendor:publish

L’inconvénient avec cette commande indifférenciée c’est qu’elle publie aussi les notifications et la pagination de Laravel, ce que l’on ne désire pas forcément :

On peut supprimer les publications correspondantes de façon manuelle :

On doit ensuite mettre à jour les routes pour les API (app\Providers\RouteServiceProvider.php méthode mapApiRoutes), si on veut générer des API :

Route::group([
    'middleware' => 'api',
    'namespace' => $this->namespace."\\API",
    'prefix' => 'api',
    'as' => 'api.',
], function ($router) {
    require base_path('routes/api.php');
});

De la même manière si on veut générer des API il faut faire cette publication supplémentaire :

php artisan infyom:publish

Ce qui a pour effet de créer ces deux fichiers :

Et voilà tout est en place, on peut commencer à s’amuser !

Interface graphique

A la base le générateur fonctionne à partir de la console en lignes de commandes mais il existe un GUI actuellement en version alpha. Dans la documentation on trouve une mise en garde :

Generator GUI Builder is currently in Beta and its not in a sync with current laravel-generator package. We highly recommend to use command line interface.

Donc à utiliser avec prudence mais ça serait franchement dommage de s’en priver !

Pour l’installer c’est tout simple :

"require": {
 "infyomlabs/generator-builder": "dev-master"
}

Avec un petit :

composer update

Un ajout dans config/app.php :

\InfyOm\GeneratorBuilder\GeneratorBuilderServiceProvider::class,

Et deux publications :

php artisan vendor:publish 
php artisan infyom.publish:generator-builder

On dispose alors d’une belle interface graphique :

C’est franchement plus accueillant que la console !

Les commandes

On dispose des commandes suivantes avec artisan :

php artisan infyom:api $MODEL_NAME
php artisan infyom:scaffold $MODEL_NAME
php artisan infyom:api_scaffold $MODEL_NAME

La première est pour une API, la seconde pour du scaffold, la dernière pour les deux. On va voir des exemples bientôt !

Classiquement les entrées se font à partir de la console mais on a deux autres possibilités :

  • à partir d’un fichier JSON, on a un exemple ici :

En voici un extrait :

{
    "name": "title",
    "dbType": "string",
    "htmlType": "text",
    "validations": "required",
    "searchable": true
},
  • à partir d’une table existante :
php artisan infyom:scaffold $MODEL_NAME --fromTable --tableName=$TABLE_NAME

Cette option est intéressante quand on a déjà une base constituée !

On dispose également de commandes individuelles pour générer :

  • la migration (php artisan infyom:migration $MODEL_NAME)
  • le modèle (php artisan infyom:model $MODEL_NAME )
  • le repository (php artisan infyom:repository $MODEL_NAME)
  • le contrôleur d’API (php artisan infyom.api:controller $MODEL_NAME)
  • les requêtes API (php artisan infyom.api:requests $MODEL_NAME)
  • les tests (php artisan infyom.api:tests $MODEL_NAME)
  • le scaffold (php artisan infyom.scaffold:controller $MODEL_NAME)
  • les requêtes de formulaire (php artisan infyom.scaffold:requests $MODEL_NAME)
  • les vues (php artisan infyom.scaffold:views $MODEL_NAME)

On peut ainsi générer ou régénérer de manière très fine ce dont on a besoin.

Scaffold

Commençons avec un scaffold avec la console. Par exemple on veut une table de villes avec : nom et nombre d’habitants. Un truc simple pour nos essais :

php artisan infyom:scaffold Ville

On a alors ces générations :

Donc on a :

  • la migration
  • le modèle
  • le repository
  • la requête de formulaire
  • le contrôleur
  • les vues
  • les routes

On a la possibilité dans la foulée d’avoir la migration :

Voyons un peu tout ça…

La migration

Le code généré :

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;

class CreateVillesTable extends Migration
{

    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('villes', function (Blueprint $table) {
            $table->increments('id');
            $table->string('nom');
            $table->integer('habitants');
            $table->timestamps();
            $table->softDeletes();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::drop('villes');
    }
}

On a la génération automatique de l’id et des timestamps. On voit également que par défaut on a le soft delete. Dans la documentation il est précisé le contraire… d’ailleurs si je regarde dans la configuration (config/infyom/laravel_generator.php) :

'options' => [
    'softDelete' => true,
    ...
]

La documentation n’est pas tout à fait à jour…

Le modèle

Pour le modèle on trouve un dossier app/Models :

Avec ce code :

<?php

namespace App\Models;

use Eloquent as Model;
use Illuminate\Database\Eloquent\SoftDeletes;

/**
 * Class Ville
 * @package App\Models
 * @version January 9, 2017, 5:02 pm UTC
 */
class Ville extends Model
{
    use SoftDeletes;

    public $table = 'villes';
    

    protected $dates = ['deleted_at'];


    public $fillable = [
        'nom',
        'habitants'
    ];

    /**
     * The attributes that should be casted to native types.
     *
     * @var array
     */
    protected $casts = [
        'nom' => 'string',
        'habitants' => 'integer'
    ];

    /**
     * Validation rules
     *
     * @var array
     */
    public static $rules = [
        'nom' => 'required|max:255',
        'habitants' => 'required|numeric'
    ];

    
}

On retrouve logiquement les informations pour le soft delete.

On peut remarquer que les règles de validation figurent dans le modèle.

Enfin on trouve la propriété $casts qui permet de fixer le type d’une donnée lorsqu’on y accède.

On trouve aussi les annotations pour Swagger.

Le repository

Il y a création d’un dossier app/Repositories :

On trouve ce code :

<?php

namespace App\Repositories;

use App\Models\Ville;
use InfyOm\Generator\Common\BaseRepository;

class VilleRepository extends BaseRepository
{
    /**
     * @var array
     */
    protected $fieldSearchable = [
        'nom',
        'habitants'
    ];

    /**
     * Configure the Model
     **/
    public function model()
    {
        return Ville::class;
    }
}

On n’y trouve pas grand chose mais les champs sur lesquels pourront porter une recherche et une méthode qui permet d’avoir le modèle.

Ca semble bien vide mais… On voit que notre repository étend InfyOm\Generator\Common\BaseRepository qu’on peut trouver ici :

On y trouve quelques méthodes :

  • findWithoutFail
  • create
  • update
  • updateRelations

Et ce repository de base étend lui-même Prettus\Repository\Eloquent\BaseRepository. que vous pouvez trouver sur Github :

C’est un package très fourni et intéressant qui mériterait à lui seul un article complet. On se retrouve ainsi avec de très nombreuses méthodes disponibles !

Les requêtes de formulaire

On en trouve deux (création et mise à jour) :

Le code est le même pour les deux :

<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;
use App\Models\Ville;

class CreateVilleRequest 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 Ville::$rules;
    }
}

Comme les règles de validation sont dans le modèle on va les chercher là.

Le contrôleur

Là le code est assez fourni :

<?php

namespace App\Http\Controllers;

use App\Http\Requests\CreateVilleRequest;
use App\Http\Requests\UpdateVilleRequest;
use App\Repositories\VilleRepository;
use App\Http\Controllers\AppBaseController;
use Illuminate\Http\Request;
use Flash;
use Prettus\Repository\Criteria\RequestCriteria;
use Response;

class VilleController extends AppBaseController
{
    /** @var  VilleRepository */
    private $villeRepository;

    public function __construct(VilleRepository $villeRepo)
    {
        $this->villeRepository = $villeRepo;
    }

    /**
     * Display a listing of the Ville.
     *
     * @param Request $request
     * @return Response
     */
    public function index(Request $request)
    {
        $this->villeRepository->pushCriteria(new RequestCriteria($request));
        $villes = $this->villeRepository->all();

        return view('villes.index')
            ->with('villes', $villes);
    }

    /**
     * Show the form for creating a new Ville.
     *
     * @return Response
     */
    public function create()
    {
        return view('villes.create');
    }

    /**
     * Store a newly created Ville in storage.
     *
     * @param CreateVilleRequest $request
     *
     * @return Response
     */
    public function store(CreateVilleRequest $request)
    {
        $input = $request->all();

        $ville = $this->villeRepository->create($input);

        Flash::success('Ville saved successfully.');

        return redirect(route('villes.index'));
    }

    /**
     * Display the specified Ville.
     *
     * @param  int $id
     *
     * @return Response
     */
    public function show($id)
    {
        $ville = $this->villeRepository->findWithoutFail($id);

        if (empty($ville)) {
            Flash::error('Ville not found');

            return redirect(route('villes.index'));
        }

        return view('villes.show')->with('ville', $ville);
    }

    /**
     * Show the form for editing the specified Ville.
     *
     * @param  int $id
     *
     * @return Response
     */
    public function edit($id)
    {
        $ville = $this->villeRepository->findWithoutFail($id);

        if (empty($ville)) {
            Flash::error('Ville not found');

            return redirect(route('villes.index'));
        }

        return view('villes.edit')->with('ville', $ville);
    }

    /**
     * Update the specified Ville in storage.
     *
     * @param  int              $id
     * @param UpdateVilleRequest $request
     *
     * @return Response
     */
    public function update($id, UpdateVilleRequest $request)
    {
        $ville = $this->villeRepository->findWithoutFail($id);

        if (empty($ville)) {
            Flash::error('Ville not found');

            return redirect(route('villes.index'));
        }

        $ville = $this->villeRepository->update($request->all(), $id);

        Flash::success('Ville updated successfully.');

        return redirect(route('villes.index'));
    }

    /**
     * Remove the specified Ville from storage.
     *
     * @param  int $id
     *
     * @return Response
     */
    public function destroy($id)
    {
        $ville = $this->villeRepository->findWithoutFail($id);

        if (empty($ville)) {
            Flash::error('Ville not found');

            return redirect(route('villes.index'));
        }

        $this->villeRepository->delete($id);

        Flash::success('Ville deleted successfully.');

        return redirect(route('villes.index'));
    }
}

Le contrôleur utilise intensément le repository. Pour les sessions c’est la package Laracasts\Flash\Flash qui a été choisi.

Les routes

Les routes sont toutes générées avec :

Route::resource('villes', 'VilleController');

Les vues

On se retrouve avec 7 vues pour nos villes :

Action !

Il ne nous reste plus qu’à voir tout ça en oeuvre !

Création

Pour la création d’une ville on a donc l’url …/villes/create :

C’est plutôt élégant et je me retrouve bien avec un champ numérique pour les habitants :

La validation fonctionne :

La liste

Pour avoir la liste des villes l’url est …/villes :

On dispose des boutons classiques pour voir, modifier et supprimer. D’autre part il y a un bouton pour la création d’une nouvelle ville.

Lorsqu’on crée le scaffold il y a la possibilité d’ajouter la pagination, pour ça il suffit de prévoir l’option :

php artisan infyom:scaffold Ville --paginate=10

Je n’ai pas essayé…

La suppression

Pour la suppression c’est une fenêtre Javascript classique pour la confirmation :

Une API

La seconde possibilité est donc de créer une API. La commande est très proche :

php artisan infyom:api Ville

On a les mêmes saisies :

Et presque les mêmes générations :

Au niveau des différences on a les requêtes de formulaires rangées dans un dossier API :

Le contrôleur lui aussi dans un dossier API :

Et évidemment maintenant on ne renvoie plus des vues mais des réponses JSON :

return $this->sendResponse($villes->toArray(), 'Villes retrieved successfully');

Les routes sont créées dans routes/api.php.

Si on veut le scaffold en plus de l’API la commande est celle-ci :

php artisan infyom:api_scaffold Ville

Génération à partir d’une table existante

Une possibilité intéressante est celle qui consiste à générer le code à partir d’une table existante :

php artisan infyom:scaffold Ville --fromTable --tableName=villes

Et ça fonctionne :

Par contre la migration n’est pas crée, ça pourrait pourtant être intéressant pour des modifications. D’autre part il faut aussi renseigner les règles de validation dans les modèles.

Les relations

Il est très fréquent et pour ne pas dire incontournable d’avoir des relations entre les tables. Qu’a à nous proposer Infyom dans ce domaine ?

On peut définir des relations lors de la création (documentation ici). Alors essayons :

php artisan infyom:scaffold Post --relations

Là je crée un modèle Post en relation (1 à plusieurs) avec un modèle Comment. Au niveau du modèle généré j’ai bien la relation :

/**
 * @return \Illuminate\Database\Eloquent\Relations\HasMany
 **/
public function comments()
{
    return $this->hasMany(\App\Models\Comment::class, 'post_id');
}

Il me faut maintenant le second modèle (Comment) :

php artisan infyom:scaffold Comment --relations

La migration est correcte :

public function up()
{
    Schema::create('comments', function (Blueprint $table) {
        $table->increments('id');
        $table->text('content');
        $table->integer('post_id')->unsigned();
        $table->timestamps();
        $table->softDeletes();
        $table->foreign('post_id')->references('id')->on('posts');
    });
}

La relation aussi :

/**
 * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
 **/
public function post()
{
    return $this->belongsTo(\App\Models\Post::class, 'post_id');
}

J’ai fait aussi un essai avec des tables existantes et les relations sont très bien générées dans les modèles !

Evidemment au niveau du scaffold on a les deux tables gérés séparément sans tenir compte de la relation.

Le repository de base

Un aspect important d’Infyom est l’utilisation du package andersao/l5-repository dont j’ai commencé à parler pour le repository. Le débat sur la pertinence des repositoy n’est pas clos et ne le sera sans doute jamais, mais là la question n’est pas d’actualité parce qu’on en dispose d’office, alors autant aller voir tout ce qui nous est offert. Il est à noter que la documentation d’Infyom est plutôt laconique sur le sujet, alors creusons un peu.

Les méthodes de base

Déja à la base on a à disposition de nombreuses méthodes :

  • all($columns = array(‘*’))
  • first($columns = array(‘*’))
  • paginate($limit = null, $columns = [‘*’])
  • find($id, $columns = [‘*’])
  • findByField($field, $value, $columns = [‘*’])
  • findWhere(array $where, $columns = [‘*’])
  • findWhereIn($field, array $where, $columns = [*])
  • findWhereNotIn($field, array $where, $columns = [*])
  • create(array $attributes)
  • update(array $attributes, $id)
  • updateOrCreate(array $attributes, array $values = [])
  • delete($id)
  • orderBy($column, $direction = ‘asc’);
  • with(array $relations);
  • has(string $relation);
  • whereHas(string $relation, closure $closure);
  • hidden(array $fields);
  • visible(array $fields);
  • scopeQuery(Closure $scope);
  • getFieldsSearchable();
  • setPresenter($presenter);
  • skipPresenter($status = true);

C’est ce qui permet d’utiliser dans un contrôleur généré :

$posts = $this->postRepository->all();

On a ainsi tous les posts. On voit qu’on pourrait spécifier les colonnes qu’on désire. Par exemple avec :

$posts = $this->postRepository->all('title');

On se retrouve avec juste les titres :

Pour l’affichage des posts (show) c’est la méthode findWithoutFail qui est utilisée :

$post = $this->postRepository->findWithoutFail($id);

Celle-là on la trouve dans le repository de base d’Infyom :

public function findWithoutFail($id, $columns = ['*'])
{
    try {
        return $this->find($id, $columns);
    } catch (Exception $e) {
        return;
    }
}

Elle utilise la méthode find du repository du package. On voit qu’on peut aussi préciser les colonnes désirées.

De la même manière dans les contrôleurs on trouve les méthodes create, update et delete pour les différentes opérations.

Il y a même une méthode prévue (with) pour l' »eager loading ».

Les critères

Dans les cas réels les méthodes de base sont rapidement débordées et on a besoin de plus de précision dans les requêtes. C’est là qu’interviennent les critères. Prenons un exemple encore avec les posts :

use Prettus\Repository\Contracts\RepositoryInterface;
use Prettus\Repository\Contracts\CriteriaInterface;

class PostTest implements CriteriaInterface {

    public function apply($model, RepositoryInterface $repository)
    {
        return $model->where('content', 'test');
    }
}

Je construit le critère :  je ne veux que les enregistrements où la colonne content est test. Je peux ensuite utiliser le critère dans le contrôleur :

$this->postRepository->pushCriteria(PostTest::class);

Et ça marche :

Un autre type de critère intéressant est celui qui prend l’url comme référence, le critère de requête. Il est d’ailleurs mis en place dans le contrôleur généré si on veut la recherche :

$this->postRepository->pushCriteria(new RequestCriteria($request));

Pour obtenir le même résultat que le critère vu ci-dessus il suffit d’utiliser l’url :

../posts?search=content:test

Les possibilités sont assez riches, il y a de nombreux exemples dans la documentation.

Le package Fractal est également installé et peut donc être utilisé. Le but est de modifier les données de la base pour avoir un affichage correct pour l’utilisateur, ce qui est judicieux pour les API.

Conclusion

Infyom est intéressant parce qu’il fait gagner beaucoup de temps. Il est dommage qu’il ne propose pas une interface graphique comme Laravel Schema Designer ! Par contre il va bien plus loin dans la génération du code. L’idéal serait une combinaison de ces deux outils…

D’autre part l’installation automatisée du template de backend est intéressante.

Laisser un commentaire