Professional Documents
Culture Documents
@extends('layouts.app')
@section('content')
<div class="container">
<div class="row">
<div class="col-md-8 col-md-offset-2">
<div class="panel panel-default">
<div class="panel-heading">Login</div>
<div class="panel-body">
<form class="form-horizontal" method="POST"
action="{{ route('login') }}">
{{ csrf_field() }}
<div class="form-group{{
$errors->has('email') ? ' has-error' : '' }}">
<label for="email" class="col-md-4
control-label">E-Mail Address</label>
<div class="col-md-6">
<input id="email" type="email"
class="form-control" name="email" value="{{ old('email') }}"
required autofocus>
@if ($errors->has('email'))
<span class="help-block">
<strong>{{
$errors->first('email') }}</strong>
</span>
@endif
</div>
</div>
<div class="form-group{{
$errors->has('password') ? ' has-error' : '' }}">
<label for="password" class="col-md-4
control-label">Password</label>
<div class="col-md-6">
<input id="password"
type="password" class="form-control" name="password" required>
@if ($errors->has('password'))
<span class="help-block">
<strong>{{
$errors->first('password') }}</strong>
</span>
@endif
</div>
</div>
<div class="form-group">
<div class="col-md-6 col-md-offset-4">
<div class="checkbox">
<label>
<input type="checkbox"
name="remember" {{ old('remember') ? 'checked' : '' }}> Remember
Me
</label>
</div>
</div>
</div>
<div class="form-group">
<div class="col-md-8 col-md-offset-4">
<button type="submit" class="btn
btn-primary">
Login
</button>
LaravelCollective
Le premier package auquel on pense est celui de LaravelCollective.
Je rappelle qu’historiquement cette fonctionnalité était intégrée
à Laravel et a été supprimée avec la version 5 pour devenir un
package indépendant.
@if ($errors->has('email'))
<span class="help-block">
<strong>{{ $errors->first('email') }}</strong>
</span>
@endif
</div>
</div>
@if ($errors->has('password'))
<span class="help-block">
<strong>{{ $errors->first('password')
}}</strong>
</span>
@endif
</div>
</div>
<div class="form-group">
<div class="col-md-6 col-md-offset-4">
<div class="checkbox">
<label>
{!! Form::checkbox('remember', old('remember')
? 'checked' : '') !!} Remember Me
</label>
</div>
</div>
</div>
<div class="form-group">
<div class="col-md-8 col-md-offset-4">
{!! Form::submit('Login', ['class' => 'btn btn-
primary']) !!}
{!! link_to_route('password.request', $title = 'Forgot
Your Password?', [], ['class' => 'btn btn-link']) !!}
</div>
</div>
@if ($errors->has($name))
<span class="help-block">
<strong>{{ $errors->first($name) }}</strong>
</span>
@endif
Form::component('bsValidationError', 'components.validation-
error', ['name']);
Bootstraper
Le package patricktalmadge/bootstrapper est une collection de
classes qui permettent de générer tout ce qu’il faut pour
Bootstrap. Voilà qui doit être intéressant pour notre vue !
Bootstrapper\BootstrapperL5ServiceProvider::class,
Par contre on trouve une page pour les panels. La syntaxe est
simple :
Panel::normal()
->withHeader('Normal')
->withBody('Panel body')
->footer('Panel footer')
{!! Panel::normal()
->withHeader('Login')
->withBody('Mon body')
!!}
Mais dans notre cas ça nous donne plus de souci qu’autre chose…
{!! ControlGroup::generate(
Form::label('email', 'E-Mail Address'),
Form::email('email', old('email'), ['required' => true,
'autofocus' => true]),
$errors->has('email') ? Form::help($errors->first('email')) :
'',
4
)->withAttributes($errors->has('email') ? ['class' => 'has-
error'] : [])
!!}
{!! ControlGroup::generate(
Form::label('password', 'Password'),
Form::password('password', old('password'), ['required' =>
true]),
$errors->has('password') ?
Form::help($errors->first('password')) : '',
4
)->withAttributes($errors->has('password') ? ['class' => 'has-
error'] : [])
!!}
Je retrouve presque le même aspect, je n’arrive pas à imposer les
6 colonnes sur l’input mais pour le reste ça passe.
@extends('layouts.app')
@section('content')
<div class="container">
<div class="row">
<div class="col-md-8 col-md-offset-2">
{!! Panel::normal()
->withHeader('Login')
->withBody(
Form::horizontal(['route' => 'login']) .
ControlGroup::generate(
Form::label('email', 'E-Mail
Address'),
Form::email('email', old('email'),
['required' => true, 'autofocus' => true]),
$errors->has('email') ?
Form::help($errors->first('email')) : '',
4
)->withAttributes($errors->has('email') ? ['class' => 'has-error']
: []) .
ControlGroup::generate(
Form::label('password', 'Password'),
Form::password('password',
old('password'), ['required' => true]),
$errors->has('password') ?
Form::help($errors->first('password')) : '',
4
)->withAttributes($errors->has('password') ? ['class' => 'has-
error'] : []) .
'<div class="form-group">
<div class="col-md-6 col-md-offset-4">
<div class="checkbox">
<label>' .
Form::checkbox('remember',
old('remember') ? 'checked' : '') . ' Remember Me
</label>
</div>
</div>
</div>
<div class="form-group">
<div class="col-md-8 col-md-offset-4">' .
Form::submit('Login', ['class' => 'btn
btn-primary']) .
link_to_route('password.request',
$title = 'Forgot Your Password?', [], ['class' => 'btn btn-link'])
.
'</div>
</div>' .
Form::close()
)
!!}
</div>
</div>
</div>
@endsection
Il faut l’installer :
Avec ce code :
<?php
namespace App\Forms;
use Kris\LaravelFormBuilder\Form;
$this
->add('email', 'email')
->add('password', 'password')
->add('remember', 'checkbox')
->add('submit', 'submit', ['label' => 'Login']);
Avec ce code :
<form method="POST" action="http://monsite.org/login" accept-
charset="UTF-8">
<input name="_token" type="hidden"
value="aQAQ3NznC0RbWAR6ZzkNCD1k8NDqeICguR2kEBSt">
<div class="form-group" >
<label for="email" class="control-label">Email</label>
<input class="form-control" name="email" type="email"
id="email">
</div>
<div class="form-group">
<label for="password" class="control-
label">Password</label>
<input class="form-control" name="password"
type="password" id="password">
</div>
<div class="form-group">
<input id="remember" name="remember" type="checkbox"
value="1">
<label for="remember" class="control-
label">Remember</label>
</div>
<button class="form-control" type="submit">Login</button>
</form>
Et la validation ?
@extends('layouts.app')
@section('content')
<div class="container">
<div class="row">
<div class="col-md-8 col-md-offset-2">
{!! Panel::normal()
->withHeader('Login')
->withBody(form($form))
!!}
</div>
</div>
</div>
@endsection
Blade
Maintenant voyons si finalement Blade n’est pas capable de gérer
élégamment tout ça !
On sait qu’avec Blade on peu inclure une vue dans une autre, on
appelle ça en général des vues partielles. Ce qui est gênant dans
notre formulaire c’est la répétition du code ici :
<div class="col-md-6">
<input id="email" type="email" class="form-control"
name="email" value="{{ old('email') }}" required autofocus>
@if ($errors->has('email'))
<span class="help-block">
<strong>{{ $errors->first('email') }}</strong>
</span>
@endif
</div>
</div>
<div class="col-md-6">
<input id="password" type="password" class="form-control"
name="password" required>
@if ($errors->has('password'))
<span class="help-block">
<strong>{{ $errors->first('password') }}</strong>
</span>
@endif
</div>
</div>
<div class="col-md-6">
<input id="{{ $name }}" type="{{ $type }}" class="form-
control" name="{{ $name }}" value="{{ old($name) }}" {{
$attributes }}>
@if ($errors->has($name))
<span class="help-block">
<strong>{{ $errors->first($name) }}</strong>
</span>
@endif
</div>
</div>
@include('partials.input', [
'name' => 'email',
'title' => 'E-Mail Address',
'type' => 'email',
'attributes' => 'required autofocus'
])
@include('partials.input', [
'name' => 'password',
'title' => 'Password',
'type' => 'password',
'attributes' => 'required'
])
Un composant panel :
<div class="form-group">
<div class="col-md-6 col-md-offset-4">
<div class="checkbox">
<label>
<input type="checkbox" name="{{ $name }}" {{
old($name) ? 'checked' : '' }}> {{ $title }}
</label>
</div>
</div>
</div>
@extends('layouts.app')
@section('content')
<div class="container">
<div class="row">
<div class="col-md-8 col-md-offset-2">
@component('components.panel')
@slot('title')
Login
@endslot
@component('components.form')
@slot('class')
form-horizontal
@endslot
@slot('url')
route('login')
@endslot
@include('partials.input', [
'name' => 'email',
'title' => 'E-Mail Address',
'type' => 'email',
'attributes' => 'required autofocus'
])
@include('partials.input', [
'name' => 'password',
'title' => 'Password',
'type' => 'password',
'attributes' => 'required'
])
@include('partials.checkbox', [
'name' => 'remember',
'title' => ' Remember Me',
])
<div class="form-group">
<div class="col-md-8 col-md-offset-4">
<button type="submit" class="btn btn-
primary">
Login
</button>
<a class="btn btn-link" href="{{
route('password.request') }}">
Forgot Your Password?
</a>
</div>
</div>
@endcomponent
@endcomponent
</div>
</div>
</div>
@endsection
Conclusion
Que faut-il retenir de ce petit tour d’horizon ? Que le sujet
reste largement ouvert et que quelque chose est vraiment à
inventer à ce niveau.
APP_NAME=Pensées
Le serveur
Données
On va donc créer une table pour mémoriser les pensées :
protected $fillable = [
'text', 'user_id',
];
<?php
Lancez tinker :
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Pensee;
$pensee = Pensee::create($request->all());
return Pensee::with('user')->find($pensee->id);
}
return response()->json();
}
}
Protections
On doit protéger les urls qui sont réservées aux utilisateurs
authentifiée, on va donc ajouter un constructeur dans le
contrôleur pour ajouter le middleware auth pour les méthodes
concernées :
<?php
namespace App\Policies;
use App\User;
use App\Pensee;
use Illuminate\Auth\Access\HandlesAuthorization;
class PenseePolicy
{
use HandlesAuthorization;
...
$pensee->delete();
return response()->json();
}
Routes
On a aussi besoin des routes pour accéder aux méthodes du
contrôleur qu’on vient de créer et pour un peu réorganiser.
Remplacez toutes les routes existantes par celles-ci :
Auth::routes();
Route::get('/', 'PenseeController@app');
Le client
On va donc passer côté client maintenant…
npm install
Vue-resource
Par défaut Vue.js n’est pas équipé pour gérer des requêtes HTTP
alors on va installer ce package :
Alors encore une petite commande :
window.Vue = require('vue');
Vue.http.headers.common['X-CSRF-TOKEN'] =
document.head.querySelector('meta[name="csrf-token"]').content;
require('./bootstrap');
window.Vue = require('vue');
var VueResource = require('vue-resource');
Vue.use(VueResource);
Vue.component('app', require('./components/App.vue'));
@extends('layouts.app')
@section('content')
<div id="app">
<app></app>
</div>
@endsection
<template>
<div class="container">
<div v-for="pensee in pensees">
<h4>{{ pensee.user.name }}</h4>
<p>{{ pensee.text }}</p>
<p>{{ pensee.created_at }}</p>
</div>
</div>
</template>
<script>
export default {
resource: null,
data () {
return {
pensees: {}
}
},
mounted () {
this.resource = this.$resource('/pensees{/id}')
this.resource.get().then((response) => {
this.pensees = response.body
})
}
}
</script>
Au niveau des data une simple variable pensees pour contenir les
pensées.
On améliore l’aspect
On va un peu améliorer l’aspect obtenu. On va créer un composant
Card :
Avec ce code :
<template>
<div class="card red lighten-2">
<div class="card-content white-text">
<span class="card-title">{{ name }}</span>
<p>{{ text }}</p>
<p><small>{{ date }}</small></p>
</div>
</div>
</template>
<script>
export default {
props: ['name', 'text', 'date']
}
</script>
<template>
<div class="container">
<div v-for="pensee in pensees">
<card :name="pensee.user.name" :text="pensee.text"
:date="pensee.created_at"></card>
</div>
</div>
</template>
<script>
import Card from './Card'
export default {
resource: null,
data () {
return {
pensees: []
}
},
mounted () {
this.resource = this.$resource('/pensees{/id}')
this.resource.get().then((response) => {
this.pensees = response.body
})
},
components: {
Card
}
}
</script>
<template>
<div class="container">
<div v-for="pensee in pensees">
<card :pensee="pensee" :user="user"
@deletePensee="deletePensee"></card>
</div>
</div>
</template>
<script>
import Card from './Card'
export default {
resource: null,
data () {
return {
pensees: [],
user: 0
}
},
mounted () {
this.resource = this.$resource('/pensees{/id}')
this.resource.get().then(response => {
this.pensees = response.body[0]
this.user = response.body[1]
})
},
components: {
Card
},
methods: {
deletePensee (id) {
this.resource.delete({id: id}).then(response => {
let index = _.findIndex(this.pensees, function(o) {
return o.id == id; })
this.pensees.splice(index, 1)
})
}
}
}
</script>
<template>
<div class="card red lighten-2">
<div class="card-content white-text">
<span class="card-title">{{ pensee.user.name }}</span>
<p>{{ pensee.text }}</p>
<p><small>{{ pensee.created_at }}</small></p>
</div>
<div v-if="deletable" class="card-action">
<a href="#" @click.prevent="deletePensee">Supprimer cette
pensée</a>
</div>
</div>
</template>
<script>
export default {
props: ['pensee', 'user'],
computed: {
deletable () {
return this.pensee.user_id == this.user
}
},
methods: {
deletePensee () {
this.$emit('deletePensee', this.pensee.id)
}
}
}
</script>
<!DOCTYPE html>
<html lang="{{ app()->getLocale() }}">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-
scale=1">
@auth
<ul id="dropdown1" class="dropdown-content">
<li>
<a href="{{ route('logout') }}"
onclick="event.preventDefault();
document.getElementById('logout-
form').submit();">
Deconnexion
</a>
<nav>
<div class="nav-wrapper">
<a href="{{ url('/') }}" class="brand-
logo"> {{ config('app.name', 'Laravel') }}</a>
<a href="#" data-activates="mobile-demo"
class="button-collapse"><i class="material-icons">menu</i></a>
@guest
<ul class="right hide-on-med-and-down">
<li><a href="{{ route('login')
}}">Connexion</a></li>
<li><a href="{{ route('register')
}}">Enregistrement</a></li>
</ul>
<ul class="side-nav" id="mobile-demo">
<li><a href="{{ route('login')
}}">Connexion</a></li>
<li><a href="{{ route('register')
}}">Enregistrement</a></li>
</ul>
@else
<ul class="right hide-on-med-and-down">
<li><a class="dropdown-button" href="#!"
data-activates="dropdown1">{{ Auth::user()->name }}<i
class="material-icons right">arrow_drop_down</i></a></li>
</ul>
@endguest
</div>
</nav>
@yield('content')
</div>
<template>
<div class="container">
<div v-for="pensee in pensees" :key="pensee.id">
<card :pensee="pensee" :user="user"
@deletePensee="deletePensee"></card>
</div>
<div id="ajout" class="modal">
<form v-on:submit.prevent="addPensee">
<div class="modal-content">
<h4>Ajout d'une pensée</h4>
<hr>
<div class="input-field col s12">
<textarea id="pensee" v-model="texte"
class="materialize-textarea"></textarea>
<label for="pensee">Entrez ici votre
pensée</label>
<p class="red-text">{{ error }}</p>
</div>
</div>
<div class="modal-footer">
<button class="btn waves-effect waves-light"
type="submit">Envoyer
<i class="material-icons right">send</i>
</button>
</div>
</form>
</div>
</div>
</template>
<script>
import Card from './Card'
export default {
resource: null,
data () {
return {
error: '',
texte: '',
pensees: [],
user: 0
}
},
mounted () {
this.resource = this.$resource('/pensees{/id}')
this.resource.get().then(response => {
this.pensees = response.body[0]
this.user = response.body[1]
})
},
components: {
Card
},
methods: {
deletePensee (id) {
this.resource.delete({id: id}).then(response => {
let index = _.findIndex(this.pensees, function(o) {
return o.id == id; })
this.pensees.splice(index, 1)
})
},
addPensee () {
this.resource.save({ text: this.texte }).then(response
=> {
$('#ajout').modal('close')
this.texte = ''
this.error = ''
this.pensees.unshift(response.body)
}, response => {
this.error = response.body.errors.text[0]
})
}
}
}
</script>
<template>
<div class="card red lighten-2">
<div class="card-content white-text">
<span class="card-title">{{ pensee.user.name }}</span>
<p>{{ pensee.text }}</p>
<p><small>{{ date }}</small></p>
</div>
<div v-if="deletable" class="card-action">
<a href="#" @click.prevent="deletePensee">Supprimer cette
pensée</a>
</div>
</div>
</template>
<script>
import moment from 'moment'
moment.locale('fr')
export default {
props: ['pensee', 'user'],
computed: {
deletable () {
return this.pensee.user_id == this.user
},
date () {
return moment(pensee.created_at).format('D MMMM YYYY à
H:mm:ss')
}
},
methods: {
deletePensee () {
this.$emit('deletePensee', this.pensee.id)
}
}
}
</script>
On importe la librairie :
On fixe la locale :
moment.locale('fr')
La pagination
Si on commence à avoir beaucoup de pensées il va nous falloir une
pagination :
<template>
<div class="container">
<paginate name="pensees" :list="pensees" :per="3">
<li v-for="pensee in paginated('pensees')"
:key="pensee.id">
<card :pensee="pensee" :user="user"
@deletePensee="deletePensee"></card>
</li>
</paginate>
<paginate-links for="pensees" :classes="{'ul':
'pagination'}"></paginate-links>
<div id="ajout" class="modal">
<form v-on:submit.prevent="addPensee">
<div class="modal-content">
<h4>Ajout d'une pensée</h4>
<hr>
<div class="input-field col s12">
<textarea id="pensee" v-model="texte"
class="materialize-textarea"></textarea>
<label for="pensee">Entrez ici votre
pensée</label>
<p class="red-text">{{ error }}</p>
</div>
</div>
<div class="modal-footer">
<button class="btn waves-effect waves-light"
type="submit">Envoyer
<i class="material-icons right">send</i>
</button>
</div>
</form>
</div>
</div>
</template>
<script>
import Card from './Card'
import VuePaginate from 'vue-paginate'
export default {
resource: null,
data () {
return {
error: '',
texte: '',
pensees: [],
paginate: ['pensees'],
user: 0
}
},
mounted () {
this.resource = this.$resource('/pensees{/id}')
this.resource.get().then(response => {
this.pensees = response.body[0]
this.user = response.body[1]
})
},
components: {
Card
},
methods: {
deletePensee (id) {
this.resource.delete({id: id}).then(response => {
let index = _.findIndex(this.pensees, function(o) {
return o.id == id; })
this.pensees.splice(index, 1)
})
},
addPensee () {
this.resource.save({ text: this.texte }).then(response
=> {
$('#ajout').modal('close')
this.texte = ''
this.error = ''
this.pensees.unshift(response.body)
}, response => {
this.error = response.body.errors.text[0]
})
}
}
}
</script>
En résumé
Laravel n’impose rien au niveau de la gestion côté client et ce ne
sont pas les solutions qui manquent. Toutefois l’installation de
Laravel prévoit par défaut une intendance pour Vue.js. On a vu
dans ce chapitre que cette intendance est au point et que tout
fonctionne de façon efficace. La prise en main de Vue.js est moins
difficile que d’autres framework comme Angular.
Reste que le choix des outils de gestion côté client n’est pas
évident. Souvent JQuery est amplement suffisant lorsqu’on a juste
à générer des requêtes HTTP et à manipuler un peu le DOM. A quel
moment devient-il plus intéressant de passer à un framework plus
complet ? La réponse n’est pas facile parce que dépendant de
plusieurs facteurs dont la maîtrise qu’on a d’un outil particulier
n’est pas le moindre.
npm install
{
"private": true,
"scripts": {
"dev": "npm run development",
"development": "cross-env NODE_ENV=development
node_modules/webpack/bin/webpack.js --progress --hide-modules --
config=node_modules/laravel-mix/setup/webpack.config.js",
"watch": "cross-env NODE_ENV=development
node_modules/webpack/bin/webpack.js --watch --progress --hide-
modules --config=node_modules/laravel-
mix/setup/webpack.config.js",
"watch-poll": "npm run watch -- --watch-poll",
"hot": "cross-env NODE_ENV=development
node_modules/webpack-dev-server/bin/webpack-dev-server.js --inline
--hot --config=node_modules/laravel-mix/setup/webpack.config.js",
"prod": "npm run production",
"production": "cross-env NODE_ENV=production
node_modules/webpack/bin/webpack.js --progress --hide-modules --
config=node_modules/laravel-mix/setup/webpack.config.js"
},
"devDependencies": {
"axios": "^0.16.2",
"bootstrap-sass": "^3.3.7",
"cross-env": "^5.0.1",
"jquery": "^3.1.1",
"laravel-mix": "^1.0",
"lodash": "^4.17.4",
"vue": "^2.1.10"
}
}
mix.js('resources/assets/js/app.js', 'public/js')
.sass('resources/assets/sass/app.scss', 'public/css');
@import "~bootstrap-sass/assets/stylesheets/bootstrap";
require('bootstrap-sass');
On passe à Materialize
L’intendance
Sur le site de npm on cherche le module :
Et comme on veut ce module uniquement pour le développement on va
utiliser cette syntaxe :
"devDependencies": {
...
"materialize-css": "^0.100.2",
...
}
// Fonts
//@import
url("https://fonts.googleapis.com/css?family=Raleway:300,400,600")
;
@import
url("https://fonts.googleapis.com/icon?family=Material+Icons");
// Variables
//@import "variables";
// Bootstrap
//@import "~bootstrap-sass/assets/stylesheets/bootstrap";
//Materialize
@import "~materialize-css/sass/materialize.scss";
//require('bootstrap-sass');
require('materialize-css');
<!DOCTYPE html>
<html lang="{{ app()->getLocale() }}">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-
scale=1">
@auth
<ul id="dropdown1" class="dropdown-content">
<li>
<a href="{{ route('logout') }}"
onclick="event.preventDefault();
document.getElementById('logout-
form').submit();">
Logout
</a>
<nav>
<div class="nav-wrapper">
<a href="{{ url('/') }}" class="brand-
logo"> {{ config('app.name', 'Laravel') }}</a>
<a href="#" data-activates="mobile-demo"
class="button-collapse"><i class="material-icons">menu</i></a>
@guest
<ul class="right hide-on-med-and-down">
<li><a href="{{ route('login')
}}">Login</a></li>
<li><a href="{{ route('register')
}}">Register</a></li>
</ul>
<ul class="side-nav" id="mobile-demo">
<li><a href="{{ route('login')
}}">Login</a></li>
<li><a href="{{ route('register')
}}">Register</a></li>
</ul>
@else
<ul class="right hide-on-med-and-down">
<li><a class="dropdown-button" href="#!"
data-activates="dropdown1">{{ Auth::user()->name }}<i
class="material-icons right">arrow_drop_down</i></a></li>
</ul>
<ul class="right hide-on-med-and-down">
<li><a class="dropdown-button" href="#!"
data-activates="dropdown1">{{ Auth::user()->name }}<i
class="material-icons right">arrow_drop_down</i></a></li>
</ul>
@endguest
</div>
</nav>
@yield('content')
</div>
Login
Voici la nouvelle vue de login
(resources/views/auth/login.blade.php) :
@extends('layouts.app')
@section('css')
<style>
.card {
margin-top: 40px;
}
</style>
@endsection
@section('content')
<div class="container">
<div class="row">
<div class="card">
<form method="POST" action="{{ route('login') }}">
<div class="card-content">
{{ csrf_field() }}
<span class="card-title">Login</span>
<hr>
<div class="row">
<div class="input-field col s12">
<i class="material-icons
prefix">mail</i>
<input id="email" type="email"
name="email" value="{{ old('email') }}" class="{{
$errors->has('email') ? 'invalid' : '' }}" required autofocus>
<label for="email" data-error="{{
$errors->has('email') ? $errors->first('email'): '' }}">E-Mail
Address</label>
</div>
</div>
<div class="row">
<div class="input-field col s12">
<i class="material-icons
prefix">lock</i>
<input id="password" type="password"
name="password" class="{{ $errors->has('password') ? 'invalid' :
'' }}" required>
<label for="password" data-error="{{
$errors->has('password') ? $errors->first('password'): ''
}}">Password</label>
</div>
</div>
<p>
<input type="checkbox" id="remember" {{
old('remember') ? 'checked' : '' }}>
<label for="remember">Remember Me</label>
</p>
</div>
<div class="card-action">
<button class="btn waves-effect waves-light"
type="submit" name="action">Login
<i class="material-icons
right">lock_open</i>
</button>
<a class="waves-effect waves-light btn"
href="{{ route('password.request') }}">Forgot Your Password?<i
class="material-icons right">message</i></a>
</div>
</form>
</div>
</div>
</div>
@endsection
Avec cet aspect :
Register
Voici la nouvelle vue d’enregistrement
(resources/views/auth/register.blade.php) :
@extends('layouts.app')
@section('css')
<style>
.card {
margin-top: 40px;
}
</style>
@endsection
@section('content')
<div class="container">
<div class="row">
<div class="card">
<form method="POST" action="{{ route('register') }}">
<div class="card-content">
{{ csrf_field() }}
<span class="card-title">Register</span>
<hr>
<div class="row">
<div class="input-field col s12">
<i class="material-icons
prefix">person</i>
<input id="name" type="text"
name="name" value="{{ old('name') }}" class="{{
$errors->has('name') ? 'invalid' : '' }}" required autofocus>
<label for="name" data-error="{{
$errors->has('name') ? $errors->first('name'): '' }}">Name</label>
</div>
</div>
<div class="row">
<div class="input-field col s12">
<i class="material-icons
prefix">mail</i>
<input id="email" type="email"
name="email" value="{{ old('email') }}" class="{{
$errors->has('email') ? 'invalid' : '' }}" required autofocus>
<label for="email" data-error="{{
$errors->has('email') ? $errors->first('email'): '' }}">E-Mail
Address</label>
</div>
</div>
<div class="row">
<div class="input-field col s12">
<i class="material-icons
prefix">lock</i>
<input id="password" type="password"
name="password" class="{{ $errors->has('password') ? 'invalid' :
'' }}" required>
<label for="password" data-error="{{
$errors->has('password') ? $errors->first('password'): ''
}}">Password</label>
</div>
</div>
<div class="row">
<div class="input-field col s12">
<i class="material-icons
prefix">lock</i>
<input id="password-confirm"
type="password" name="password_confirmation" required>
<label for="password-confirm">Confirm
Password</label>
</div>
</div>
</div>
<div class="card-action">
<button class="btn waves-effect waves-light"
type="submit" name="action">Register
<i class="material-icons right">create</i>
</button>
</div>
</form>
</div>
</div>
</div>
@endsection
Email
Voici la nouvelle vue de demande de renouvellement du mot de passe
(resources/views/auth/passwords/email.blade.php) :
@extends('layouts.app')
@section('css')
<style>
.row > .card {
margin-top: 40px;
}
</style>
@endsection
@section('content')
<div class="container">
<div class="row">
<div class="card">
<form method="POST" action="{{
route('password.email') }}">
<div class="card-content">
{{ csrf_field() }}
@if (session('status'))
<div class="card green darken-1">
<div class="card-content white-text">
{{ session('status') }}
</div>
</div>
@endif
<span class="card-title">Reset Password</span>
<hr>
<div class="row">
<div class="input-field col s12">
<i class="material-icons
prefix">mail</i>
<input id="email" type="email"
name="email" value="{{ old('email') }}" class="{{
$errors->has('email') ? 'invalid' : '' }}" required autofocus>
<label for="email" data-error="{{
$errors->has('email') ? $errors->first('email'): '' }}">E-Mail
Address</label>
</div>
</div>
</div>
<div class="card-action">
<button class="btn waves-effect waves-light"
type="submit" name="action">Send Password Reset Link
<i class="material-icons
right">lock_open</i>
</button>
</div>
</form>
</div>
</div>
</div>
@endsection
Reset
Voici la nouvelle vue de de renouvellement du mot de passe
(resources/views/auth/passwords/reset.blade.php) :
@extends('layouts.app')
@section('css')
<style>
.card {
margin-top: 40px;
}
</style>
@endsection
@section('content')
<div class="container">
<div class="row">
<div class="card">
<form method="POST" action="{{
route('password.request') }}">
<div class="card-content">
{{ csrf_field() }}
Home
Pour finir voilà le code pour la vue
resources/views/home.blade.php :
@extends('layouts.app')
@section('content')
<div class="container">
<div class="row">
<div class="col s12 m6">
@if (session('status'))
<div class="card green darken-1">
<div class="card-content white-text">
{{ session('status') }}
</div>
</div>
@endif
<div class="card red lighten-2">
<div class="card-content white-text">
<span class="card-title">Dashboard</span>
You are logged in!
</div>
</div>
</div>
</div>
</div>
@endsection
utilisation de variables
imbrication de code
importations
mixins (groupe de déclaration CSS réutilisable)
héritage
opérateurs…
On peut très bien se contenter de la console pour générer le CSS à
partir par exemple d’un fichier Sass :
Installation
Pour utiliser Laravel Mix vous devez disposer de NPM, donc aussi
de node.js. Si vous n’êtes pas trop sûr d’avoir ça sur votre
machine entrez ça dans votre console :
node -v
npm -v
@import "~bootstrap-sass/assets/stylesheets/bootstrap";
npm install
Utilisation
Maintenant vous pouvez utiliser le fichier webpack.mix.js pour
préciser ce que vous voulez compiler. Par défaut on a ce code :
/*
|----------------------------------------------------------------
----------
| Mix Asset Management
|----------------------------------------------------------------
----------
|
| Mix provides a clean, fluent API for defining some Webpack
build steps
| for your Laravel application. By default, we are compiling the
Sass
| file for the application as well as bundling up all the JS
files.
|
*/
mix.js('resources/assets/js/app.js', 'public/js')
.sass('resources/assets/sass/app.scss', 'public/css');
Ce qui a pour effet de créer (en fait ici régénérer parce qu’il
existe déjà) le fichier app.css (et aussi celui du Javascript mais
j’en parlerai plus loin) :
html {
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
}
body {
margin: 0;
}
mix.js('resources/assets/js/app.js', 'public/js')
Les commandes sont strictement les mêmes que celles qu’on a vues
pour le CSS puisque les opérations sont groupées.
require('./bootstrap');
window.Vue = require('vue');
Vue.component('example', require('./components/Example.vue'));
<template>
<div class="container">
<div class="row">
<div class="col-md-8 col-md-offset-2">
<div class="panel panel-default">
<div class="panel-heading">Example
Component</div>
<div class="panel-body">
I'm an example component!
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
mounted() {
console.log('Component mounted.')
}
}
</script>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="{{ asset('css/app.css') }}">
</head>
<body>
<div id="app">
<example></example>
</div>
<script type="text/javascript" src="{{ asset('js/app.js')
}}"></script>
</body>
</html>
Et voici le résultat :
npm install
require('./components/Example');
Avec ce code :
<div className="panel-body">
I'm an example component!
</div>
</div>
</div>
</div>
</div>
);
}
}
if (document.getElementById('example')) {
ReactDOM.render(<Example />,
document.getElementById('example'));
}
Vous pouvez tester ce composant avec la même vue que ci-dessus
puisqu’il a le même nom.
mix.copy('node_modules/foo/bar.css', 'public/css/bar.css');
mix.copyDirectory('assets/img', 'public/img');
En résumé
Laravel Mix permet de compiler du CSS.
Laravel Mix permet de compiler du Javascript.
Laravel comporte une intendance pour l’utilisation de
Vue.js.
Une commande artisan permet de remplacer l’intendance de
Vue.js pour celle de React.
local
Azure
AWS S3
Dropbox
FTP
Rackspace…
La configuration
La configuration se trouve dans le fichier config/filesystem.php
avec ce code par défaut :
'disks' => [
'local' => [
'driver' => 'local',
'root' => storage_path('app'),
],
'public' => [
'driver' => 'local',
'root' => storage_path('app/public'),
'url' => env('APP_URL').'/storage',
'visibility' => 'public',
],
's3' => [
'driver' => 's3',
'key' => env('AWS_KEY'),
'secret' => env('AWS_SECRET'),
'region' => env('AWS_REGION'),
'bucket' => env('AWS_BUCKET'),
],
],
La façade
Une fois que vous avez défini vos disques dans la configuration
vous pouvez utiliser la façade Storage pour gérer les fichiers. Il
suffit de préciser le disque concerné par la manipulation (s’il
n’est pas précisé ça sera le disque par défaut défini dans la
configuration). Par exemple avec :
Storage::disk('s3')->get(image.png);
'disks' => [
'local' => [
'driver' => 'local',
'root' => storage_path('app'),
],
'public' => [
'driver' => 'local',
'root' => storage_path('app/public'),
'url' => env('APP_URL').'/storage',
'visibility' => 'public',
],
'files' => [
'driver' => 'local',
'root' => public_path() . ('/files'),
'visibility' => 'public',
],
'thumbs' => [
'driver' => 'local',
'root' => public_path() . ('/thumbs'),
'visibility' => 'public',
],
...
],
'route' => [
'prefix' => 'elfinder',
'middleware' => ['web', 'redac'],
],
/**
* Get user files directory
*
* @return string|null
*/
public function getFilesDirectory()
{
if ($this->role === 'redac') {
$folderPath = 'user' . $this->id;
if (!in_array($folderPath ,
Storage::disk('files')->directories())) {
Storage::disk('files')->makeDirectory($folderPath);
}
return $folderPath;
}
return null;
}
Storage::disk('files')->directories()
Les thumbs
Lorsqu’on liste les articles dans l’administration on visualise
une miniature de l’image d’illustration de l’article :
if (!in_array($dir , Storage::disk('thumbs')->directories())) {
Storage::disk('thumbs')->makeDirectory($dir);
}
$image = Image::make(url($model->image))->widen(100);
Storage::disk('thumbs')->put(substr_replace
(self::makeThumbPath($path), '', 0, 7), $image->encode());
Le cache
Laravel fournit une API unique pour tous les drivers de cache
gérés comme Memcached ou Redis. Ça fonctionne un peu comme les
sessions avec un système clé-valeur. Par défaut le cache utilise
un fichier.
La configuration
Le fichier de configuration est ici :
'stores' => [
'apc' => [
'driver' => 'apc',
],
'array' => [
'driver' => 'array',
],
'database' => [
'driver' => 'database',
'table' => 'cache',
'connection' => null,
],
'file' => [
'driver' => 'file',
'path' => storage_path('framework/cache/data'),
],
'memcached' => [
'driver' => 'memcached',
'persistent_id' => env('MEMCACHED_PERSISTENT_ID'),
'sasl' => [
env('MEMCACHED_USERNAME'),
env('MEMCACHED_PASSWORD'),
],
'options' => [
// Memcached::OPT_CONNECT_TIMEOUT => 2000,
],
'servers' => [
[
'host' => env('MEMCACHED_HOST', '127.0.0.1'),
'port' => env('MEMCACHED_PORT', 11211),
'weight' => 100,
],
],
],
'redis' => [
'driver' => 'redis',
'connection' => 'default',
],
],
Mettre en cache
Pour mettre en cache on utilise la méthode put :
Cache::forever('clé', 'valeur');
ou cache()->forever('clé', 'valeur');
$valeur = Cache::get('clé');
ou
$valeur = cache('clé');
Cache::forget('clé');
ou
cache()->forget('clé');
Cache::flush();
ou
cache()->flush();
if (Cache::has('clé')) {
//
}
ou
if (cache()->has('clé')) {
//
}
Un exemple
Vous pouvez trouver un exemple expliqué avec une démonstration en
ligne ici. L’article date un peu mais reste pertinent.
La configuration
Ce que j’ai dit ci-dessus pour les routes est tout aussi valable
pour la configuration et on dispose de ces deux commandes :
config:cache
config:clear
En résumé
Laravel est équipé d’un système de gestion de fichiers
unifié qui permet des manipulations en local ou sur le cloud
Laravel est équipé d’un système de cache unifié
Laravel autorise la mise en cache des routes
"require-dev": {
"filp/whoops": "~2.0",
"fzaninotto/faker": "~1.4",
"mockery/mockery": "0.9.*",
"phpunit/phpunit": "~6.0"
},
Mais vous pouvez aussi utiliser le fichier phar que vous pouvez
trouver sur cette page et le placer à la racine de votre
application et vous êtes prêt à tester !
php vendor\phpunit\phpunit\phpunit -h
L’intendance de Laravel
Si vous regardez les dossiers de Laravel vous allez en trouver un
qui est consacré aux tests :
<?php
namespace Tests;
Cette classe est chargée de créer une application pour les tests
dans un environnement spécifique (ce qui permet de mettre en place
une configuration adaptée aux tests).
Toutes les classes de test que vous allez créer devront étendre
cette classe TestCase.
<?php
namespace Tests\Unit;
use Tests\TestCase;
use Illuminate\Foundation\Testing\RefreshDatabase;
<?php
namespace Tests\Feature;
use Tests\TestCase;
use Illuminate\Foundation\Testing\RefreshDatabase;
$response->assertStatus(200);
}
}
Pourquoi 2 dossiers ?
Sans entrer pour le moment dans le code sachez simplement que dans
le premier exemple qu’on se contente de demander si un truc vrai
est effectivement vrai (bon c’est sûr que ça devrait être vrai
^^). Dans le second on envoie une requête pour la route de base et
on attend une réponse positive (200).
Pour lancer ces tests c’est très simple, entrez cette commande :
On voit qu’ont été effectués 2 tests et 2 assertions et que tout
s’est bien passé.
L’environnement de test
Je vous ai dit que les tests s’effectuent dans un environnement
particulier, ce qui est bien pratique.
<testsuite name="Unit">
<directory suffix="Test.php">./tests/Unit</directory>
</testsuite>
</testsuites>
<filter>
<whitelist processUncoveredFilesFromWhitelist="true">
<directory suffix=".php">./app</directory>
</whitelist>
</filter>
<php>
<env name="APP_ENV" value="testing"/>
<env name="CACHE_DRIVER" value="array"/>
<env name="SESSION_DRIVER" value="array"/>
<env name="QUEUE_DRIVER" value="sync"/>
</php>
</phpunit>
DB_CONNECTION=mysql
Construire un test
Les trois étapes d’un test
Pour construire un test on procède généralement en trois étapes :
$result = array_sum($data);
On teste le résultat :
$this->assertEquals(60, $result);
Route::get('/', function () {
return 'coucou';
});
Route::get('/', function () {
return view('welcome')->with('message', 'Vous y êtes !');
});
{{ $message }}
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
Route::get('welcome', 'WelcomeController@index');
<?php
namespace Tests\Feature;
use Tests\TestCase;
use Illuminate\Foundation\Testing\RefreshDatabase;
Pour faire des tests efficaces il faut bien les isoler, donc
savoir ce qu’on teste, ne tester qu’une chose à la fois et ne pas
mélanger les choses.
"require-dev": {
"filp/whoops": "~2.0",
"fzaninotto/faker": "~1.4",
"mockery/mockery": "0.9.*",
"phpunit/phpunit": "~6.0"
},
<?php
namespace App\Http\Controllers;
use App\Services\Livre;
<?php
namespace App\Services;
class Livre
{
public function getTitle() {
return 'Titre';
}
}
Bon d’accord ce n’est pas très joli mais c’est juste pour la
démonstration…
<?php
namespace Tests\Feature;
use Tests\TestCase;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Collection;
use App\Services\Livre;
use Mockery;
// Création lien
$this->app->instance('\App\Services\Livre', $mock);
// Action
$response = $this->call('GET', 'welcome');
// Assertions
$response->assertSuccessful();
$response->assertViewHas('titre', 'Titre');
{{ $titre }}
$mock = Mockery::mock(Livre::class)
->shouldReceive('getTitle')
->andReturn('Titre');
$this->app->instance('\App\Services\Livre', $mock);
$response->assertSuccessful();
$response->assertViewHas('titre', 'Titre');
Installation
Il faut commencer par l’installer :
Vous allez vous retrouver avec un nouveau dossiers dans les tests
:
APP_URL=http://monsite.dev
Exemples
On va voir ça en œuvre avec l’application d’exemple. Voici
l’ensemble des fichiers de test :
<?php
namespace Tests\Browser;
use Tests\DuskTestCase;
use Laravel\Dusk\Browser;
/**
* Test login by email
* @group login
*
* @return void
*/
public function testLoginByEmail()
{
$this->browse(function (Browser $browser) {
$browser->visit('/login')
->type('log', 'redac@la.fr')
->type('password', 'redac')
->press('Login')
->assertPathIs('/')
->assertSee('Logout')
->clickLink('Logout');
});
}
/**
* Test login fail
* @group login
*
* @return void
*/
public function testLoginFail()
{
$this->browse(function (Browser $browser) {
$browser->visit('/login')
->type('log', 'toto@la.fr')
->type('password', 'toto')
->press('Login')
->assertSee('These credentials do not match
our records');
});
}
}
Et si tout va bien :
$browser->visit('/login')
->type('log', 'Slacker')
->type('password', 'slacker')
->press('Login')
->assertPathIs('/')
->clickLink('Logout')
->assertSee('Login');
En résumé
Laravel utilise PHPUnit pour effectuer les tests unitaires.
En plus des méthodes de PHPUnit on dispose d’helpers pour
intégrer les tests dans une application réalisée avec
Laravel.
Le composant Mockery permet de simuler le comportement d’une
classe et donc de bien isoler les tests.
Laravel propose des commandes conviviales pour tester une
application avec dusk.