WIS - Symfony

Renaud Bredy
renaud.bredy@kibatic.com
bredy.renaud@gmail.com
WIS Grenoble 3
2020 v1

Objectifs du module
- Comprendre les avantages & inconvénients d'un framework
- Acquérir de la culture générale du dev backend
- Intervenir dans une application qui exploite une BDD
- Comprendre l'architecture du framework symfony, et comprendre leurs responsabilités dans les bugs.
Evaluation
- 2 notes de Contrôle continu
- 1 évaluation
- 1 QCM
1. Découverte du projet
Installation des logiciels
- Installer WAMP : php7.4, MySQL et apache
- Configurer son fichier /etc/hosts
- Configurer le vhost
- Installer PHPStorm
- Installer composer
- Installer Git
Installation
Cloner le projet
git clone https://github.com/gototog/wis_20-21.git
Installer les librairies externes
composer install
Initialiser la base de données
php bin/console doctrine:database:create php bin/console doctrine:migration:migrate php bin/console doctrine:fixtures:load
Structure
- bin/ : executables (console)
- config/ : configurations
- public/ : fichiers accessibles (index.php, css, images, uploads...)
- src/ : Notre code source
- templates/ : code de présentation (html)
- tests/ : Tests du code source
- translations/ : traductions
- var/ : fichiers temporaires, fichiers générés (cache, logs...)
- vendor/ : libs externes

2. Hello world
Controleur et routing
La page Hello Name
L'url /hello/Renaud affichera "Hello Renaud"
L'url /hello/ affichera "Hello world"
La page Hello Worlds
On va afficher plusieurs fois le mot world
L'url /hello/4 affichera "Hello world world world world"
Mais, je souhaite que la route hello_name soit toujours accessible!
3. Hello {{ world }}
Templates Twig
Twig
Documentation https://twig.symfony.com Introduction https://symfony.com/doc/current/templating.html
return $this->render('hello/hello_name.html.twig', array(
'name' => $nameParameter,
));
<html><body>
<h1>hello {{ name }}</h1>
</body></html>
Modifions notre contrôleur
Et créons le fichier twig dans
templates/hello/hello_name.html.twig
Rajouter du style (1/3)
Objectifs:
- Le design est déjà intégré sur la homepage. https://startbootstrap.com/themes/clean-blog/
- Utiliser le système d'extension de Twig pour avoir le même style sur toutes nos pages
Rajouter du style (2/3)
On utiliser asset() pour charger les fichier présents dans public/
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="">
<meta name="author" content="">
<title>{% block title %}Cours de Symfony{% endblock %}</title>
{% block stylesheets %}{% endblock %}
<link rel="icon" type="image/x-icon" href="{{ asset('favicon.ico') }}"/>
<link href="{{ asset('vendor/bootstrap/css/bootstrap.css') }}" rel="stylesheet">
<link href="{{ asset('css/blog-home.css') }}" rel="stylesheet">
</head>
Rajouter du style (3/3)
A vous d'intégrer les pages Hello world!
Sans dupliquer le code, faire en sorte que le contenu soit contenu dans le design général du site

Lien vers la page Hello
Changer un item du menu en lien Hello
La fonction path génère une route via son nom
<a href="{{ path('hello_world') }}">Hello</a>
Et on peut lui passer des paramètres
{{ path('hello_name', {'name': 'Jean' }) }}
4. Les entités
Le modèle et la base de données
Configuration de la bdd
.env privé et perso
.env.dist publique et commité, liste les params
bin/console doctrine:database:create
L'objet Category
- Sous src/Entity, Créer la classe Category (Id, Name)
- L'instancier dans un BlogController
public function homepage()
{
$category = new Category();
$category->setName('Symfony');
return $this->render('blog/hompage.html.twig', array(
'categories' => array($category)
));
}
3. L'afficher dans le template twig
{% for category in categories %}
<b>votre html</b>
{% endfor %}
Doctrine

object-relational mapping
Category lié à la BDD
Recréer Category en utilisant la commande Symfony
bin/console make:entity
-> on créé une entité Category
avec un attribut name
Répercuter les changements de schéma avec
bin/console doctrine:schema:update
--dump-sql puis --force
Vérifier les changements en base de données
Récupérer les catégories
- Mettre quelques catégories dans la bdd
- Récupérer les catégories
-
- Les afficher dans la homepage

//src/Controller/HomepageController
$categories = $this->getDoctrine()->getRepository(Category::class)->findAll();
Repository
- Un repository centralise tout ce qui touche à la récupération des entités.
- JAMAIS de requête SQL ailleurs que dans un repository.
- Requetes en Doctrine Query Language (DQL).
- Méthodes magiques:
$article = $repository->find(1);
$articles = $repository->findAll();
$articles = $repository->findBy(
array('author' => 'Alexandre'), // Criteres
array('date' => 'desc'), // Tri
5, // Limite
0 // Offset
);
DQL
Vision objet du SQL
N'est pas lié a un SGBD (mysql, postgres, oracle...)
public function findByCategoryAndYear(Category $category, $year)
{
$qb = $this->createQueryBuilder('article');
$qb->where('article.category = :category')
->andWhere('article.date BETWEEN :date_start AND :date_end')
->orderBy('article.date', 'DESC')
->setParameter('category', $category)
->setParameter('date_start', new \Datetime($year . '-01-01'))
->setParameter('date_end', new \Datetime($year . '-12-31'))
;
return $qb
->getQuery()
->getResult()
;
}
Exercice (1/3)
Créer la classe Article (Id, title, content , createdAt)
Mettre un Article de test dans la base de données
Exercice (2/3)
- Récupérer le dernier article dans le contrôleur Homepage
- L'afficher dans la homepage
Exercice (3/3)
Doc http://symfony.com/doc/current/doctrine/associations.html
- Grace à la doc (OneToMany et ManyToOne), lier l'article à la catégorie : Un article a une seule catégorie, une catégorie à plusieurs articles.
- Mettre à jour nos données en base
- Dans la homepage, afficher le nom de la catégorie pour chaque article
Lazy Loading
5. MVC
Modèle
Gère les données .
Son rôle est d'aller récupérer les informations dans la base de données, de les organiser et de les assembler.
C'est aussi son role de modifier les données.
On y trouve donc (entre autres) les requêtes SQL.
C'est nos Repositories et nos Entities

Vue
Récupère des variables et les affiche, sans les modifier ou effectuer des traitements.
Principalement du code HTML, CSS et Javascript.
Très peu de PHP pour de l'algorithmie simple: boucles et conditions.

Contrôleur
Fait le lien entre la vue et le modèle.
Le contrôleur décide en fonction d'un utilisateur, de ses droits et des paramètres des requêtes quels traitements doivent être effectués.
Il ne doit pas faire de traitement et doit faire le moins de ligne possible ;)
6. Services
L'injection de dépendance et l'architecture orienté service
Sans service
$repository = new ArticleRepository(
new EntityManager(
new Connection([
'password' => '***'
'user' => 'symfony'
'database' => 'symfony'
'host' => 'localhost'
]),
new MySQLDriver(...)
),
new ClassMetadata(Article::class)
);
Avec service
$repository = $this->getContainer()->get(ArticleRepository::class)
class NewsletterManager
{
private $mailer;
public function __construct(\Mailer $mailer)
{
$this->mailer = $mailer;
}
}
Service
Classe qui rempli une fonction (responsabilité).
Elle est enregistrée en tant que service par configuration.
Le conteneur de services organise et instancie tous vos services, grâce à leur configuration.
L'injection de dépendances est assurée par le conteneur, qui connaît les arguments (TypeHint) dont a besoin un service pour fonctionner, et les lui donne donc à sa création.
Plus de détail OpenClassRoom
7. CRUD
Create
Read
Update
Delete
Persister, modifier et supprimer les entités
public function createAction()
{
$entityManager = $this->getDoctrine()->getManager();
$category = new Category();
$category->setName('Keyboard');
// tells Doctrine you want to (eventually) save the Category (no queries yet)
$entityManager->persist($category);
// actually executes the queries (i.e. the INSERT query)
$entityManager->flush();
return new Response('Saved new category with id ' . $category->getId());
}
Persister
public function updateAction($categoryId)
{
$entityManager = $this->getDoctrine()->getManager();
$product = $entityManager->getRepository(Category::class)->findOneById($categoryId);
// comme la category provient de l'entityManager
// elle est déjà référencée par Doctrine (pas besoin de persist() )
$product->setName('New Name');
$entityManager->flush();
}
Mettre à jour
// on en profite pour utiliser les paramConverter :D
/**
* @Route("/category/{id}")
*/
public function deleteAction(Category $category)
{
$entityManager = $this->getDoctrine()->getManager();
$entityManager->remove($category)
$entityManager->flush();
return $this-redirectToRoute('category_index');
}
Supprimer
8. Formulaires
Améliorons notre CRUD
Doc symfony : https://symfony.com/doc/master/forms.html
Reférence form type : https://symfony.com/doc/master/reference/forms/types.html
Reférence validation : https://symfony.com/doc/master/reference/constraints.html
class CategoryType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
// On ne permet pas de modifier l'id
// On permet de modifier l'attribut name, et ce sera un widget text
->add('name', TextType::class, array(
'required' => true,
))
// Ce champ n'est pas mappé.
->add('save', SubmitType::class)
;
}
public function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
$resolver->setDefaults([
'data_class' => Category::class,
]);
}
}
FormType
{{ form_start(form, {'attr': {'class': 'form-horizontal'}}) }}
{# Les erreurs générales du formulaire. #}
{{ form_errors(form) }}
{# Génération du form_label + form_errors + form_widget pour un champ. #}
{{ form_row(form.date) }}
{# Génération manuelle du form_row: #}
<div class="form-group">
{{ form_label(form.title, "Titre de l'annonce", {'label_attr': {'class': 'col-sm-2'}}) }}
{{ form_errors(form.title) }}
<div class="col-sm-10">
{{ form_widget(form.title, {'attr': {'class': 'form-control'}}) }}
</div>
</div>
Affichage
sans personnalisation
{{ form(form) }}
{# ou #}
{{ form_start(form, {'attr': {'novalidate': 'novalidate'}}) }}
{{ form_widget(form) }}
{{ form_end(form) }}
Création
public function create(Request $request)
{
$category = new Category();
// Créé le formulaire
$form = $this->createForm(CategoryType::class, $category);
// Hydrate l'objet lié au formulaire avec les données de la request
$form->handleRequest($request);
if ($form->isSubmitted()) {
if ($form->isValid()) {
$em = $this->getDoctrine()->getManager();
$em->persist($category));
$em->flush());
return $this->redirectToRoute("category_index");
}
}
return $this->render('category/create.html.twig', [
// /!\ we render a view instance of the form
"form" => $form->createView(),
]);
}
Validation
//src/entity/Category.php
// on importes les annotations Constraints/XXX
use Symfony\Component\Validator\Constraints as Assert;
// [...]
class Category
{
// [...]
/**
* @Assert\Length(min=2)
* @Assert\NotBlank()
* @ORM\Column(name="name", type="string", length=255)
*/
private $name;
On définit nos règles de validation
puis un service viendra valider les données
@Assert\Contrainte(option1="valeur1", option2="valeur2", …)
Syntaxe:
ATTENTION de ne pas confondre validation coté serveur et validation html5 coté navigateur (pas sécurisé)
Fichier (1/4)
// class src/Enttiy/Article
/**
* Contiendra le nom de l'image en bdd
* @ORM\Column(type="string")
*/
private $image;
/**
* Contiendra l'objet image du formulaire
* @Assert\Image()
*/
private $imageFile;
// [...] Pensez aux getter et setter
On stocke seulement le chemin relatif en bdd : le fichier, publique, ira dans le dossier public/uploads
// class src/Form/ArticleFormType
$builder->add('imageFile', FileType::class);
On ajoute le champ au formulaire
Fichier (2/4)
if ($form->isSubmitted() && $form->isValid()) {
// $file stores the uploaded Image file
/** @var Symfony\Component\HttpFoundation\File\UploadedFile $file */
$file = $article->getImageFile();
$fileName = md5(uniqid()) . '.' . $file->guessExtension();
// moves the file to the directory where image are stored
$file->move(
$this->getParameter('kernel.project_dir') . '/public/uploads/article',
$fileName
);
// updates the 'image' property to store the filename
$article->setImage($fileName);
On déplace le fichier et on ne stocke que le nom en bdd
Maintenant pour afficher l'image dans les templates
<img src="{{ asset('uploads/article/' ~ article.image) }}"/>
Fichier (3/4)
// src/Uploader/FileUploader.php
class FileUploader
{
private $targetDirectory;
public function __construct($targetDirectory)
{
$this->targetDirectory = $targetDirectory;
}
public function upload(UploadedFile $file)
{
$fileName = md5(uniqid()).'.'.$file->guessExtension();
$file->move($this->getTargetDirectory(), $fileName);
return $fileName;
}
}
On va alléger le contrôleur en factorisant le code dans un service.
Fichier (4/4)
# config/services.yml
services:
# ...
App\Uploader\FileUploader:
arguments:
$targetDirectory: '%kernel.project_dir%/public/uploads/article'
Notre controlleur
if ($form->isSubmitted() && $form->isValid()) {
// on remplace tout le code de l'upload de fichier par:
$fileName = $this->articleFileUploader->upload($article->getImageFile())
$article->setImage($fileName);
private $articleFileUploader;
public function __construct(FileUploader $articleFileUploader)
{
$this->articleFileUploader = articleFileUploader;
}
9. Sécurité
Gestion des droits et utilisateurs
Authentification : firewall
Différencier un utilisateur ou un anonyme
But du firewall: Comment identifier un utilisateur
Autorisation : roles
L'Utilisateur a plusieurs rôles qui lui sont attachés, lors de l'authentification.
On code pour faire en sorte qu'une ressource (URL/Controller...) soit accessible qu'à certains rôles.
Firewall, Authentication, Authorization
Zone protégés: Zone libre accès:
Bureaux, Dancefloor, Salle 25+ Parking, jardin extérieur
On rentre pas si on est trop pauvre.
On rentre gratuitement, coup de tampon sur la main.
On montre sa carte d'identité pour aller dans la salle 25+.
Les bureaux uniquement pour le Staff
Le cas "Vieux Manoir"
Firewall, Authentication, Authorization
/dancefloor Anonymes
/admin Employés only
/exterieur Pas sous firewall
dancefloor via un tampon
StaffOnly via une clé
Salle des vieux via Carte d'identité
On peut accéder à la salle des vieux (car dans le Firewall du dancefloor) mais on va nous demander de nous identifier: 403 (forbidden)
On ne peut pas accéder au staff: 401 (Unauthorized)
Fonctionnement

Fonctionnement

Notre sécurité http
security:
firewalls:
main:
anonymous: ~
http_basic: ~
Firewall: authentification http
security:
access_control:
- { path: ^/hello, roles: ROLE_ADMIN } # routes qui commencent par hello
Contrôle d'accès
# Sécuriser un controller ou une action
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
/** @Security("has_role('ROLE_ADMIN')") */
Nos utilisateurs
# config/packages/security.yaml
security:
providers:
in_memory:
memory:
users:
user: { password: pass, roles: [ 'ROLE_USER' ] }
admin: { password: pass, roles: [ 'ROLE_ADMIN' ] }
encoders:
# Pour tous nos utilisateur, le mot de passe est en clair (BEURK)
Symfony\Component\Security\Core\User\User: plaintext
role_hierarchy:
ROLE_ADMIN: ROLE_USER
Le rôle admin hérite du ROLE_USER
Verifier les droits dans TWIG
{% if is_granted('ROLE_ADMIN') %}
<li class="nav-item">
<a class="nav-link" href="{{ path('hello_name', { 'name' : 'Renaud' }) }}">Hello</a>
</li>
{% endif %}
Installer un bundle
# installe la librairie dans vendor.
# dans la console, tapez:
composer require friendsofsymfony/user-bundle "~2.0"
Configurer FOSUserBundle
framework:
templating:
engines: ['twig']
fos_user:
db_driver: orm
firewall_name: main
user_class: App\Entity\User
from_email:
address: noreply@codeblog.com
sender_name: Code Blog
Dans ... app/config/config.yml
On veut la configuration des routes aussi.
app/config/routing.yml
fos_user:
resource: "@FOSUserBundle/Resources/config/routing/all.xml"
Crééer notre user
Configurer le firewall
security:
encoders:
FOS\UserBundle\Model\UserInterface: bcrypt
providers:
fosuser_provider:
id: fos_user.user_provider.username
firewalls:
main:
pattern: ^/
form_login:
provider: fosuser_provider
csrf_token_generator: security.csrf.token_manager
logout: true
anonymous: true
access_control:
- { path: ^/login$, role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/register, role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/resetting, role: IS_AUTHENTICATED_ANONYMOUSLY }
Dernière ligne droite!
bin/console doctrine:schema:update --force
Allons Sur l'url /register ... C'est moche!
{% extends "base.html.twig" %}
{# Le block fos_user_content du vendor doit être affiché dans notre block body #}
{% block body %}
{% block fos_user_content %}{% endblock %}
{% endblock %}
pour surcharger n'importe quel template d'un bundle
app/Resources/{BUNDLE_NAME}/views/{PATH/TEMPLATE.html.twig}
app/Resources/FOSUserBundle/views/layout.html.twig
10. Travail noté
Système de commentaire
Système de commentaires
Ajoutez un système de commentaire pour vos articles
Scénario d'implémentation:
- Sous les articles en consultation, nous affichons la liste des commentaires
- Un utilisateur identifié clique sur le lien "commenter" sous les articles
- Il renseigne son commentaire dans un formulaire et valide. Le commentaire est sauvegardé et affiché dans la liste des commentaires.
Surtout faites vous plaisir !
Fonctionnalités bonus
- Les utilisateurs identifiés n'ont pas besoin de saisir leur adresse email et nom lorsqu'ils créent un commentaire
- Les utilisateurs peuvent supprimer leur propre commentaire
- L'administrateur peut supprimer un commentaire
- N'importe quelle idée qui vous passe par la tête
Critères d'évaluation
Le code fonctionne
Le code est compréhensible
Le code n'est pas copié sur le voisin
Le projet est rendu à temps : avant dimanche 22 décembre à 23h59m59s.
Envoyez votre projet en zip (ou un tar) à mon adresse email renaud.bredy@kibatic.com
Utilisez weTransfer si vous avez des problèmes de format ou taille de dossier
11. API - Rest

WIS Symfony
By Goto Rahoutan
WIS Symfony
Symfony beginner course
- 155