Laravel 5

Laravel et AngularJS : données et initialisation

Dans cet article nous allons voir les données et l’initialisation de l’application. Comment est géré l’utilisateur et comment l’interface s’adapte selon son statut. Il nous faut savoir dans un premier temps s’il est connecté ou pas. On doit lui donner la possibilité de se connecter, se déconnecter, de créer, modifier, supprimer un rêve. On veut aussi afficher la première page des rêves et la pagination.

Nota : cet article est le deuxième de la série, le premier se trouve ici.

Les données

Pour cette application les données sont simples puisqu’on a besoin seulement de deux tables : une pour les utilisateurs et une pour les rêves :

img97

La table users correspond à celle de l’installation de base de Laravel, j’ai juste ajouté le champ admin. Les migrations ont été constituées en conséquence.

La table dreams contient les rêves. Elle est reliée à a table users par la clé étrangère user_id. On a une relation de type 1:n entre les deux tables. Un utilisateur a plusieurs rêves mais un rêve appartient à un seul utilisateur. Les relations ont ainsi été établies dans les deux modèles. Dans User :

/**
 * The hasMany relation.
 * 
 */
public function dreams()
{
    return $this->hasMany('App\Dream');
}

Et dans Dream :

/**
 * The belongsTo relation.
 * 
 */
public function user()
{
    return $this->belongsTo('App\User');
}

Pour que l’application fonctionne j’ai prévu un seeder qui crée deux utilisateurs (le premier est administrateur) et 10 rêves avec un texte standard et affectés aléatoirement à chacun des deux :

<?php

use Illuminate\Database\Seeder;
use Illuminate\Database\Eloquent\Model;
use App\User;
use App\Dream;

class DatabaseSeeder extends Seeder {

    protected $lorem = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.';

    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        Model::unguard();

        User::create([
            'name' => 'admin',
            'email' => 'admin@use.fr',
            'password' => bcrypt('admin'),
            'admin' => true
        ]);

        User::create([
            'name' => 'user',
            'email' => 'user@use.fr',
            'password' => bcrypt('user')
        ]);

        foreach (range(1, 10) as $i) {
            Dream::create([
                'content' => $i . ' ' . $this->lorem,
                'user_id' => rand(1, 2)
            ]);
        }
    }

}

Donc pour les essais de l’application vous pouvez vous connecter avec l’un de ces utilisateurs.

Tout commence par une route

On a vu les routes dans le dernier article. On initialise l’application avec l’url de base ‘/’ :

Route::get('/', function(){
    return view('index');
});

On se contente de renvoyer la vue index :

img94

Cette vue comporte une en-tête pour charger toutes les librairies nécessaires : bootstrap, font-awesome, les polices Google, JQuery, grayscale et évidemment AngularJS et ses dépendances.

On initialise le contrôleur principal AppCtrl de l’application AngularJS pour tout le body :

<body id="page-top" data-spy="scroll" data-target=".navbar-fixed-top" ng-controller="AppCtrl">

Dans ce contrôleur on procède à des initialisations :

dreamsControllers.controller('AppCtrl', ['$scope', 'Log', 'Logout', 'Dream',
    function AppCtrl($scope, Log, Logout, Dream) {

// Variables
$scope.isLogged = false;
...

/* Initial log */
Log.get({},
        function success(response) {
            $scope.isLogged = response.auth;
        },
        function error(errorResponse) {
            console.log("Error:" + JSON.stringify(errorResponse));
        }
);

...

On veut savoir si l’utilisateur est connecté, il faut donc interroger le serveur à ce sujet. On a vu les services mis en place dans le dernier article. Voici celui qui correspond à cette action :

var dreamsServices = angular.module('dreamsServices', ['ngResource']);

dreamsServices.factory('Log', ['$resource',
    function ($resource) {
        return $resource("log", {}, {
            get: {method: 'GET'}
        });
    }]);

Nota : on pourrait s’arranger pour obtenir cette information dès le chargement initial de la page mais je trouve plus propre de procéder ainsi.

Côté Laravel c’est dans le contrôleur AuthController qu’on va générer la réponse :

/**
 * Get auth
 *
 * @return json
 */
public function log()
{
    return response()->json(['auth' => $this->auth->check()]);
}

On va ainsi pouvoir renseigner la variable isLogged du scope :

$scope.isLogged = response.auth;

Le menu

A quoi cela sert la variable isLogged ? En premier à paramétrer correctement le menu :

<!-- Collect the nav links, forms, and other content for toggling -->
<div class="collapse navbar-collapse navbar-right navbar-main-collapse">
    <ul class="nav navbar-nav">
        <!-- Hidden li included to remove active class from about link when scrolled up past about section -->
        <li class="hidden">
            <a href="#page-top"></a>
        </li>
        <li>
            <a class="page-scroll" href="#dreams">Dreams</a>
        </li>
        <li ng-show="isLogged">
            <a class="page-scroll" href="#auth">Add a dream</a>
        </li>                        
        <li ng-hide="isLogged">
            <a class="page-scroll" href="#auth">Login</a>
        </li>                        
        <li ng-show="isLogged">
            <a ng-click="logout()" href>Logout</a>
        </li>
    </ul>
</div>

Si l’utilisateur est connecté on obtient cet aspect :

img96

S’il ne l’est pas c’est celui-ci :

img95

Le formulaire

D’autre part on veut aussi adapter le formulaire de bas de page :

<!-- Auth Section -->
<section id="auth" class="content-section">
    <div class="download-section">
        <div class="container">
            <div class="col-lg-8 col-lg-offset-2">
                <div ng-hide="isLogged" >
                    <h2 class="text-center">Login</h2>
                    <form ng-controller="LoginCtrl" ng-submit="submit()" accept-charset="UTF-8" role="form">
                        // Formulaire de login

                    </form>                        
                    <div class="text-center">
                        <br>
                        <a href="auth/register" class="btn btn-default">I want to suscribe !</a>
                    </div>
                </div>

                <div ng-show="isLogged" >
                    <h2 class="text-center">Add a dream</h2>
                    <form ng-controller="DreamCtrl" ng-submit="submitCreate()" accept-charset="UTF-8" role="form">
                        // Formulaire de création d'un rêve

                    </form>  
                </div>
            </div>                   
        </div>
    </div>
</section>

S’il est connecté on affiche le formulaire de création d’un rêve :

img85

Sinon on lui présente le formulaire de connexion :

img78

Les rêves

On veut aussi afficher la première page des rêves dans cet emplacement :

<!-- Dreams Section -->
<section id="dreams" class="container content-section text-center">
    <div class="row">
        <div class="col-lg-10 col-lg-offset-1">
            <nav>
                <ul class="pager">
                    <li ng-show="previous" class="previous "><a ng-click="paginate('previous')" class="page-scroll" href="#dreams"><< Previous</a></li>
                    <li ng-show="next" class="next"><a ng-click="paginate('next')" class="page-scroll" href="#dreams">Next >></a></li>
                </ul>
            </nav>
            <div ng-repeat="dream in data" class="cadre">
                <h2>
                    Dream of {{ dream.user.name}}
                </h2>
                <p>{{ dream.content}}</p>                        
                <h2>
                    <div ng-if="dream.is_owner">
                        <a ng-click="edit(dream.id, $index)" href>
                            <span class="fa fa-fw fa-pencil"></span>
                        </a>
                        <a ng-click="destroy(dream.id)" href="#dreams">
                            <span class="fa fa-fw fa-trash"></span>
                        </a>
                    </div>
                </h2>
            </div>
            <nav>
                <ul class="pager">
                    <li ng-show="previous" class="previous"><a ng-click="paginate('previous')" class="page-scroll" href="#dreams"><< Previous</a></li>
                    <li ng-show="next" class="next"><a ng-click="paginate('next')" class="page-scroll" href="#dreams">Next >></a></li>
                </ul>
            </nav>
        </div>
    </div>
</section>

Essentiellement on utilise un ng-repeat qui passe en revue une variable data. Mais pour le moment cette variable est vide. On doit aussi renseigner la pagination. Revenons au contrôleur :

dreamsControllers.controller('AppCtrl', ['$scope', 'Log', 'Logout', 'Dream',
    function AppCtrl($scope, Log, Logout, Dream) {

        // Variables
        $scope.isLogged = false;
        $scope.data = {};
        $scope.page = 1;
        $scope.previous = false;
        $scope.next = false;

        /* Initial log */
        ...

        /* Pagination */
        $scope.paginate = function (direction) {
            if (direction === 'previous')
                --$scope.page;
            else if (direction === 'next')
                ++$scope.page;
            Dream.get({page: $scope.page},
            function success(response) {
                $scope.data = response.data;
                $scope.previous = response.prev_page_url;
                $scope.next = response.next_page_url;
            },
                    function error(errorResponse) {
                        console.log("Error:" + JSON.stringify(errorResponse));
                    }
            );
        };

        /* Initial page */
        $scope.paginate();

On va s’intéresser principalement à ces variables :

$scope.data = {};
$scope.page = 1;
$scope.previous = false;
$scope.next = false;

Vous remarquez la ligne :

$scope.paginate();

On appelle la méthode paginate du contrôleur. Que fait cette méthode ? Son rôle est d’envoyer une requête au serveur pour lui demander les rêves d’une page ainsi que la pagination. Le contrôleur fait appel au service :

dreamsServices.factory('Dream', ['$resource',
    function ($resource) {
        return $resource("dream/:id", {page: '@page'}, {
            get: {method: 'GET'},
            save: {method: 'POST'},
            delete: {method: 'DELETE'},
            update: {method: 'PUT'}
        });
    }]);

C’est le get qui va être utilisé ici. Suivons le processus de cette requête. Côté Laravel on arrive dans le contrôleur DreamController :

/**
 * Display a listing of the resource.
 *
 * @return Response
 */
public function index()
{
    return response()->json($this->dreamRepository->getDreamsWithUserPaginate(4));
}

On fait appel au repository :

/**
 * Get dreams with user paginate.
 *
 * @param  integer $n
 * @return collection
 */
public function getDreamsWithUserPaginate($n)
{
    $dreams = Dream::with('user')
            ->latest()
            ->simplePaginate($n);

    return $dreams;
}

On prend les rêves de la page, classés dans l’ordre des dates récentes, avec aussi leur utilisateur. Pour éviter de transmettre des données inutiles ou qui pourraient constituer des problèmes de sécurité on limite les champs transmis au niveau des modèles. Dans User on a la propriété $hidden :

/**
 * The attributes excluded from the model's JSON form.
 *
 * @var array
 */
protected $hidden = ['password', 'remember_token', 'email', 'id', 'created_at', 'updated_at'];

On a la même propriété renseignée dans Dream :

/**
 * The attributes excluded from the model's JSON form.
 *
 * @var array
 */
protected $hidden = ['created_at', 'updated_at', 'user_id'];

Les champs cachés ne seront pas présents dans la réponse JSON.

Dans le contrôleur d’AngularJS on renseigne ainsi 3 variables :

$scope.data = response.data;
$scope.previous = response.prev_page_url;
$scope.next = response.next_page_url;

On a ainsi tout ce qu’il faut pour afficher les rêves et la pagination correcte :

img77

Les icônes des rêves

Si l’utilisateur est connecté, ou si c’est un administrateur, on doit afficher les icônes de modification et de suppression :

img82

Comment cela est-il géré ? Regardons de plus près le code de la page :

<h2>
    <div ng-if="dream.is_owner">
        <a ng-click="edit(dream.id, $index)" href>
            <span class="fa fa-fw fa-pencil"></span>
        </a>
        <a ng-click="destroy(dream.id)" href="#dreams">
            <span class="fa fa-fw fa-trash"></span>
        </a>
    </div>
</h2>

On dispose pour un rêve de l’attribut is_owner. D’où sort-il celui-là ? Il n’existe évidemment pas dans la table des rêves. On va aller faire un petit tour du côté de Laravel pour comprendre. Regardez le code du modèle Dream :

<?php namespace App;

use Illuminate\Database\Eloquent\Model;

class Dream extends Model {

    /**
     * Added attribute
     *
     * @var array
     */
    protected $appends = ['is_owner'];

    ...

    /**
     * is_owner mutator.
     * 
     */
    public function getIsOwnerAttribute()
    {
        if(auth()->check())
        {
            return $this->attributes['user_id'] === auth()->id() || auth()->user()->admin;
        }
        
        return false;
    }

}

On voit qu’on ajoute l’attribut is_owner :

protected $appends = ['is_owner'];

On le définit dans cet « accessor » :

public function getIsOwnerAttribute()
{
    if(auth()->check())
    {
        return $this->attributes['user_id'] === auth()->id() || auth()->user()->admin;
    }
    
    return false;
}

On retourne true si l’utilisateur connecté est l’auteur du rêve ou si on a affaire à un administrateur. Exactement ce qu’il nous faut pour savoir quand afficher les icônes !

Dans le prochain article on verra comment fonctionnent le login et le logout.

Print Friendly, PDF & Email

Laisser un commentaire