Renaud Bredy
renaud.bredy@kibatic.com
bredy.renaud@gmail.com
WIS Grenoble 3
2020 v1
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
Controleur et routing
L'url /hello/Renaud affichera "Hello Renaud"
L'url /hello/ affichera "Hello world"
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!
Templates 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
Objectifs:
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>
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
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' }) }}
Le modèle et la base de données
.env privé et perso
.env.dist publique et commité, liste les params
bin/console doctrine:database:create
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 %}
object-relational mapping
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
//src/Controller/HomepageController
$categories = $this->getDoctrine()->getRepository(Category::class)->findAll();
$article = $repository->find(1);
$articles = $repository->findAll();
$articles = $repository->findBy(
array('author' => 'Alexandre'), // Criteres
array('date' => 'desc'), // Tri
5, // Limite
0 // Offset
);
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()
;
}
Créer la classe Article (Id, title, content , createdAt)
Mettre un Article de test dans la base de données
Doc http://symfony.com/doc/current/doctrine/associations.html
Lazy Loading
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
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.
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 ;)
L'injection de dépendance et l'architecture orienté service
$repository = new ArticleRepository(
new EntityManager(
new Connection([
'password' => '***'
'user' => 'symfony'
'database' => 'symfony'
'host' => 'localhost'
]),
new MySQLDriver(...)
),
new ClassMetadata(Article::class)
);
$repository = $this->getContainer()->get(ArticleRepository::class)
class NewsletterManager
{
private $mailer;
public function __construct(\Mailer $mailer)
{
$this->mailer = $mailer;
}
}
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
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());
}
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();
}
// 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');
}
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,
]);
}
}
{{ 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>
sans personnalisation
{{ form(form) }}
{# ou #}
{{ form_start(form, {'attr': {'novalidate': 'novalidate'}}) }}
{{ form_widget(form) }}
{{ form_end(form) }}
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(),
]);
}
//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é)
// 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
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) }}"/>
// 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.
# 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;
}
Gestion des droits et utilisateurs
Différencier un utilisateur ou un anonyme
But du firewall: Comment identifier un utilisateur
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.
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"
/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)
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')") */
# 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
{% if is_granted('ROLE_ADMIN') %}
<li class="nav-item">
<a class="nav-link" href="{{ path('hello_name', { 'name' : 'Renaud' }) }}">Hello</a>
</li>
{% endif %}
# installe la librairie dans vendor.
# dans la console, tapez:
composer require friendsofsymfony/user-bundle "~2.0"
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"
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 }
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
Système de commentaire
Ajoutez un système de commentaire pour vos articles
Scénario d'implémentation:
Surtout faites vous plaisir !
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