Index de l'article

Cet article n'est pas l'article d'un spécialiste de Symfony. Je défriche ici les grandes possibilités du framework, dans sa version 4, en fusionnant/vulgarisant plusieurs tutos glânés sur internet. L'objectif est surtout de me construire un mémo d'apprentissage de Symfony 4, que je souhaite utiliser pour le déploiement de SCRUD plus ou moins complexes et d'API.

  • Testé en local sous Windows.
  • Wamp doit être installé, disposant d'une version de PHP 7 minimum.
  • Installez également Composer, en mode développeur, en lui mentionnant la dernière version de PHP pendant l'installation.

Composer est dorénavant indispensable pour démarrer un projet Symfony 4 (et l'Installer ne l'est plus). De même il n'est plus nécessaire de déclarer les dépendances dans le Kernel.

Il vous faudra redémarrer votre machine pour prendre en compte Composer. Ensuite la commande composer -v doit afficher des informations de version.

Intégrer PHP aux variables d'environnement Windows

Si ce n'est pas déjà fait (testez php -v) :

set PATH=%PATH%;C:\wamp\bin\php\php7.2.4

Ensuite la commande php -v doit afficher des informations de version.

 


Installation

Une installation pour un projet de test avec les principaux composants nécessaires pour débuter. Il peut y avoir des redondances.

Créer un projet Symfony

Placez-vous d'abord dans le bon répertoire de votre serveur web (wamp/www).

Pour un site web classique :

composer create-project symfony/website-skeleton nom-de-votre-projet

Ou un projet de type APIs, micro-service...

composer create-project symfony/skeleton nom-de-votre-projet

Flex et un certain nombre d'autres packages vont s'installer. Tout comme Composer, Flex est significatif pour SF4.

À ce stade, après démarrage de Wamp, vous devriez voir quelque chose en vous rendant à cette adresse :

http://localhost/nom-de-votre-projet/public/

Avec une erreur en bas de page, car la barre de débogage n'est pas encore installée.

Pour installer une version spécifique de Symfony :

composer create-project symfony/website-skeleton nom-de-votre-projet 4.1

Installer le pack Apache

Nécessaire pour beaucoup de chose, et notamment avoir de belles URLS (sans index.php). En vous plaçant d'abord dans votre projet :

composer require symfony/apache-pack

Confirmez l'installation (Yes).

Installer la Debug Toolbar

En vous plaçant d'abord dans votre projet :

composer require --dev symfony/profiler-pack

Installer tout le pack Debug

Pour bénéficier de la profiler toolbar et de beaucoup d'autres outils. En vous plaçant d'abord dans votre projet :

composer require debug

Installer Doctrine

En vous plaçant d'abord dans votre projet :

composer require symfony/orm-pack

Installerweb-server-bundle

En vous plaçant d'abord dans votre projet :

composer require --dev symfony/web-server-bundle

Installer Maker

En vous plaçant d'abord dans votre projet :

composer require doctrine maker

Installer doctrine-fixtures-bundle

composer require --dev doctrine/doctrine-fixtures-bundle

Installer Twig

En vous plaçant d'abord dans votre projet :

composer require symfony/twig-bundle

Installer Web Server (optionnel)

composer require server --dev

Installer framework-extra-bundle

En vous plaçant d'abord dans...

composer require sensio/framework-extra-bundle

Installer Symfony Form component

composer require symfony/form

Installer validator

composer require symfony/validator

Installer SwiftMailerBundle

composer require swiftmailer-bundle

Installer PHPUnit

composer req symfony/phpunit-bridge

Installer browser-kit

composer req --dev browser-kit

Installer css-selector

composer req --dev symfony/css-selector

Installer security-bundle

composer require symfony/security-bundle

Installer les Annotations

composer require annotations

Vous pouvez installer plusieurs composants en même temps :

Installer FosUser

composer require friendsofsymfony/user-bundle

Puis face à un joli message d'erreur de type :

The child node "db_driver" at path "fos_user" must be configured.

Il vous faudra suivre ce tuto : https://vfac.fr/blog/how-install-fosuserbundle-with-symfony-4


Base de données et entités

Installer Doctrine

Nous allons d'abord utiliser l'ORM Doctrine.

composer require symfony/orm-pack
composer require symfony/maker-bundle --dev

Créer la base de données

Dans le fichier .env, personnaliser la ligne DATABASE_URL de cette façon par exemple (root est l'utilisateur de base de données, et les guillemets sont le mot de passe vide) :

DATABASE_URL=mysql://root:''@127.0.0.1:3306/ma_jolie_base

Puis :

php bin/console doctrine:database:create

Qui aura pour effet de créer la base de données dans MySQL.

Créer une 1ère entité

php bin/console make:entity

Puis remplir les informations demandées (nom du 1er champ, taille, nullable). Ne créez pas de champ ID, en effet il sera automatiquement créé.

La classe de l'entité est le nom de la table. Les propriétés de l'entité sont ses champs.

Tapez une nouvelle fois Enter quand vous avez fini. Symfony a généré un joli fichier du nom de votre entité dans le répertoire src/Entity, avec les getters et setters...

Mais la table correspondant n'est pas encore créée. Pour cela :

php bin/console make:migration

Puis :

php bin/console doctrine:migrations:migrate

Confirmez la création, et allez voir en BDD. Une jolie table est née  !

À tous moment vous pouvez ajouter des champs dans votre entité, avec les mêmes commandes, en précisant l'exact même nom de classe. Symfony reconnaîtra votre table et y ajoutera les champs.

 


Relations entre entités

Dans notre cas nous avons déjà l'entité Customer. Nous allons ajouter une relation de type 1 à plusieurs vers des adresses (dans Customer, une relation OneToMany).

Créez une 2nd entité Address.

Revenez dans votre entité Customer (php bin/console make:entity puis Customer), puis créez un champ address de type relation.

Nommez customer_id le champ de stockage des customers dans la table address.

Ensuite, dans votre contrôleur CustomerAdmin, où vous appelez des champs dans le formulaire via la fonction configureFormFields, ajoutez dans la fonction ce code pour appeler la relation :

$addressFieldOptions = [
// see available options
'multiple' => true,
'label' => 'Address',
'class' => 'App\Entity\Address',
'required' => true,
'property' => 'getAdd1',
'by_reference' => true,
'translation_domain' => 'SonataUserBundle'
];
$formMapper->add('address', ModelType::class, $addressFieldOptions) ;

Afin de gérer le lien avec les id des customers au moment de l'enregistrement (dans customer_id), ajoutez cette nouvelle fonction après configureFormFields.

public function preUpdate($object)
{
foreach ($object->getAddress() as $address) {
$address->setCustomer($object);
}
}

Enfin, afin de gérer la suppression, retournez dans l'entité Customer, au niveau de la relation OneToMany, pour ajouter un cascade et un orphanRemoval.

/**
* @ORM\OneToMany(targetEntity="App\Entity\Address", mappedBy="customer", cascade={"all"}, orphanRemoval=true)
*/
private $address;

Types de champs

Liste déroulante

Pour faire d'un de vos champ une liste déroulante côté formulaire, ajoutez un use ChoiceType, et un array par exemple :

$formMapper->add('Country', TextType::class);

Devient :

$formMapper->add('country', ChoiceType::class, array(
'choices' => array(
'France' => 'France',
'United Kingdom' => 'United Kingdom',
),
));

Checkbox

...
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
...
$formMapper->add('type', CheckboxType::class, array(
'label' => 'Show this entry publicly?',
'required' => false,
));
...

Attention, il semble que les checkboxs n'acceptennt pas d'avoir 2 labels, seulement 1.


Contrôleur

Créer un contrôleur

php bin/console make:controller VotrePremierController

Un fichier PHP se crée dans src/Controller, avec le nom choisi.

Ajoutez-y la mention de votre entité lié, en ajoutant un use dans le fichier :

use App\Entity\VotreEntity;

Une page est déjà dispo dans http://localhost/Symfony-web/public/VotreControleur. Ainsi qu'un template dans src/templates.

Ajoutez également un use de type Response :

use Symfony\Component\HttpFoundation\Response;

Tester l'enregistrement en BDD

En modifiant ainsi votre contrôleur, puis en rafraîchissant la page, vous devriez pouvoir enregistrer quelques données (en dur dans l'exemple) :

  1. <?php
  2.  
  3. namespace App\Controller;
  4.  
  5. use Symfony\Component\Routing\Annotation\Route;
  6. use Symfony\Bundle\FrameworkBundle\Controller\Controller;
  7. use App\Entity\Product;
  8. use Symfony\Component\HttpFoundation\Response;
  9.  
  10. class ProductController extends Controller
  11. {
  12. /**
  13. * @Route("/product", name="product")
  14. */
  15. public function index()
  16. {
  17. // you can fetch the EntityManager via $this->getDoctrine()
  18. // or you can add an argument to your action: index(EntityManagerInterface $entityManager)
  19. $entityManager = $this->getDoctrine()->getManager();
  20.  
  21. $product = new Product();
  22. $product->setName('Keyboard');
  23. $product->setPrice(1999);
  24. $product->setDescription('Ergonomic and stylish!');
  25.  
  26. // tell Doctrine you want to (eventually) save the Product (no queries yet)
  27. $entityManager->persist($product);
  28.  
  29. // actually executes the queries (i.e. the INSERT query)
  30. $entityManager->flush();
  31.  
  32. return new Response('Saved new product with id '.$product->getId());
  33. }
  34. }

Tester l'affichage de données sur une page web

En ajoutant une fonction show dans la classe du contrôleur, avec sa route, vous pourrez afficher les données depuis les URLS de type http://localhost/Symfony-web/public/product/1 :

  1. ...
  2. /**
  3. * @Route("/product/{id}", name="product_show")
  4. */
  5. public function show($id)
  6. {
  7. $product = $this->getDoctrine()
  8. ->getRepository(Product::class)
  9. ->find($id);
  10.  
  11. if (!$product) {
  12. throw $this->createNotFoundException(
  13. 'No product found for id '.$id
  14. );
  15. }
  16.  
  17. return new Response('Check out this great product: '.$product->getName());
  18. // or render a template
  19. // in the template, print things with {{ product.name }}
  20. // return $this->render('product/show.html.twig', ['product' => $product]);
  21. }
  22. ...

Simplifier le contrôleur avec framework-extra-bundle

Si framework-extra-bundle est installé (voir chapitre Installation), vous pouvez simplifier la méthode show de votre contrôleur, qui ressemblera finalement à cela :

  1. <?php
  2.  
  3. namespace App\Controller;
  4.  
  5. use Symfony\Component\Routing\Annotation\Route;
  6. use Symfony\Component\HttpFoundation\Response;
  7. use Symfony\Bundle\FrameworkBundle\Controller\Controller;
  8. use App\Entity\Product;
  9.  
  10. class ProductController extends Controller
  11. {
  12. /**
  13. * @Route("/product", name="product")
  14. */
  15. public function index()
  16. {
  17. // you can fetch the EntityManager via $this->getDoctrine()
  18. // or you can add an argument to your action: index(EntityManagerInterface $entityManager)
  19. $entityManager = $this->getDoctrine()->getManager();
  20.  
  21. $product = new Product();
  22. $product->setName('Keyboard');
  23. $product->setPrice(1999);
  24. $product->setDescription('Ergonomic and stylish!');
  25.  
  26. // tell Doctrine you want to (eventually) save the Product (no queries yet)
  27. $entityManager->persist($product);
  28.  
  29. // actually executes the queries (i.e. the INSERT query)
  30. $entityManager->flush();
  31.  
  32. return new Response('Saved new product with id '.$product->getId());
  33. }
  34.  
  35. /**
  36. * @Route("/product/{id}", name="product_show")
  37. */
  38. public function show(Product $product)
  39. {
  40. return new Response('Check out this great product: '.$product->getName());
  41. }
  42.  
  43. }

Tester la mise à jour de données

Dans la classe de votre contrôleur :

...
/**
* @Route("/product/edit/{id}")
*/
public function update($id)
{
$entityManager = $this->getDoctrine()->getManager();
$product = $entityManager->getRepository(Product::class)->find($id);

if (!$product) {
throw $this->createNotFoundException(
'No product found for id '.$id
);
}

$product->setName('New product name!');
$entityManager->flush();
return $this->redirectToRoute('product_show', [
'id' => $product->getId()
]);
}
...

Tester la suppression de données

Toujours dans la classe du contrôleur :

...
/**
* @Route("/product/delete/{id}")
*/
public function remove($id)
{
$entityManager = $this->getDoctrine()->getManager();
$product = $entityManager->getRepository(Product::class)->find($id);
if (!$product) {
throw $this->createNotFoundException(
'No product found for id '.$id
);
}

$entityManager->remove($product);
$entityManager->flush();

return new Response('Delete product '.$id);
}
...

Concaténations

Afficher une concaténation dans un champ joint

Dans l'entité jointe, ajoutez un getter de récupération des données qui vous intéressent. Exemple pour afficher l'adresse complète d'une table address :


...
public function getFullAddress(): ?string
{
return $this->add1 . " "
.$this->add2 . ", "
.$this->zipcode . " "
.$this->city . ", "
.$this->state . " "
.$this->country;
}
...

Ensuite dans votre contrôleur, au niveau du formulaire principal par exemple, quand vous appelez le champ effectuant la jointure, vous appelez la nouvelle propriété :

$formMapper ...
$addressFieldOptions = [
...
'property' => 'getFullAddress',
...
];
$formMapper->add('address', ModelType::class, $addressFieldOptions) ;

Un début de système d'utilisateur

Projet d'exemple de création manuelle (hors FosUSer) d'un annuaire utilisateur sous Symfony (inspiré de dev-web.io).

À noter qu'au 19 juillet 2018, FosUser semblait encore incompatible avec SF4. Des packages adaptant SF4 à FosUser ou encore SonataAdmin étaient néanmoins disponibles.

1) Création du projet :

composer create-project symfony/skeleton UserDemo

2) Installez doctrine, security-bundle, twig, validator, annotations, form, swiftmailer, doctrine-fixtures-bundle, web-server-bundle et maker.

3) Dans le fichier .env :

DATABASE_URL=mysql://root:@127.0.0.1:3306/userdemo

Afin de pointer sur une BDD locale, sans mot de passe.

4) Puis créez la BDD :

php bin/console doctrine:database:create

5) Créez une 1ère entité User.

php bin/console make:entity

Sans propriété, nous allons les écrire à la main (encore merci à dev-web.io, site sur lequel vous trouverez d'autres tutos concernant Symfony 4) :

<?php
// /src/Entity/User.php
namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Validator\Constraints as Assert;

/**
* @ORM\Entity(repositoryClass="App\Repository\UserRepository")
* @ORM\Table(name="user")
* @UniqueEntity(fields="email", message="Email déjà pris")
* @UniqueEntity(fields="username", message="Username déjà pris")
*/
class User implements UserInterface, \Serializable
{
/**
* @var int
*
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*/
private $id;

/**
* @var string
*
* @ORM\Column(type="string")
* @Assert\NotBlank()
*/
private $fullName;

/**
* @var string
*
* @ORM\Column(type="string", unique=true)
* @Assert\NotBlank()
*/
private $username;

/**
* @var string
*
* @ORM\Column(type="string", unique=true)
* @Assert\NotBlank()
* @Assert\Email()
*/
private $email;

/**
* @var string
*
* @ORM\Column(type="string", length=64)
*/
private $password;

/**
* @var array
*
* @ORM\Column(type="json")
*/
private $roles = [];

public function getId(): int
{
return $this->id;
}

public function setFullName(string $fullName): void
{
$this->fullName = $fullName;
}
// le ? signifie que cela peur aussi retourner null

public function getFullName(): ?string
{
return $this->fullName;
}

public function getUsername(): ?string
{
return $this->username;
}

public function setUsername(string $username): void
{
$this->username = $username;
}

public function getEmail(): ?string
{
return $this->email;
}

public function setEmail(string $email): void
{
$this->email = $email;
}

public function getPassword(): ?string
{
return $this->password;
}

public function setPassword(string $password): void
{
$this->password = $password;
}

/**
* Retourne les rôles de l'user
*/
public function getRoles(): array
{
$roles = $this->roles;

// Afin d'être sûr qu'un user a toujours au moins 1 rôle
if (empty($roles)) {
$roles[] = 'ROLE_USER';
}
return array_unique($roles);
}
public function setRoles(array $roles): void
{
$this->roles = $roles;
}

/**
* Retour le salt qui a servi à coder le mot de passe
*
* {@inheritdoc}
*/
public function getSalt(): ?string
{
// See "Do you need to use a Salt?" at https://symfony.com/doc/current/cookbook/security/entity_provider.html
// we're using bcrypt in security.yml to encode the password, so
// the salt value is built-in and you don't have to generate one
return null;
}

/**
* Removes sensitive data from the user.
*
* {@inheritdoc}
*/
public function eraseCredentials(): void
{
// Nous n'avons pas besoin de cette methode car nous n'utilions pas de plainPassword
// Mais elle est obligatoire car comprise dans l'interface UserInterface
// $this->plainPassword = null;
}

/**
* {@inheritdoc}
*/
public function serialize(): string
{
return serialize([$this->id, $this->username, $this->password]);
}

/**
* {@inheritdoc}
*/
public function unserialize($serialized): void
{
[$this->id, $this->username, $this->password] = unserialize($serialized, ['allowed_classes' => false]);
}
}

6) Générez l'entité (créez la table) :

php bin/console doctrine:schema:update --force

7) Ajoutez un encoder :

Dans config / packages / security.yaml :

...
encoders:
App\Entity\User: bcrypt
...

8) Créez le fichier des fixtures dans src / DataFixtures / AppFixtures.php :

<?php

namespace App\DataFixtures;

use App\Entity\User;
use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Common\Persistence\ObjectManager;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;

class AppFixtures extends Fixture
{
private $passwordEncoder;
public function __construct(UserPasswordEncoderInterface $passwordEncoder)
{
$this->passwordEncoder = $passwordEncoder;
}
public function load(ObjectManager $manager)
{
foreach ($this->getUserData() as [$fullname, $username, $password, $email, $roles]) {
$user = new User();
$user->setFullName($fullname);
$user->setUsername($username);
$user->setPassword($this->passwordEncoder->encodePassword($user, $password));
$user->setEmail($email);
$user->setRoles($roles);
$manager->persist($user);
$this->addReference($username, $user);
}

$manager->flush();
}

private function getUserData(): array
{
return [
// $userData = [$fullname, $username, $password, $email, $roles];
['Jane Doe', 'jane_admin', 'kitten', Cette adresse e-mail est protégée contre les robots spammeurs. Vous devez activer le JavaScript pour la visualiser.', ['ROLE_ADMIN']],
['Tom Doe', 'tom_admin', 'kitten', Cette adresse e-mail est protégée contre les robots spammeurs. Vous devez activer le JavaScript pour la visualiser.', ['ROLE_ADMIN']],
['John Doe', 'john_user', 'kitten', Cette adresse e-mail est protégée contre les robots spammeurs. Vous devez activer le JavaScript pour la visualiser.', ['ROLE_USER']],
];
}
}

9) Chargez les fixtures :

php bin/console doctrine:fixtures:load

10) Modification du fichier config / packages / security.yml (Attention à l'indentation) :

security:
encoders:
App\Entity\User: bcrypt

providers:
database_users:
entity: { class: App\Entity\User, property: username }

firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false

main:
# les urls auxquels s'appliquent ce firewall, dans ce cas, ce sont toutes les urls
pattern: ^/
# La connexion n'est pas requise sur toutes les pages
# par exemple la page d'accueil
anonymous: true

form_login:
# Le nom de la route de la page de connexion
check_path: security_login
# Le nom de la route où se trouve le formulaire de connexion
# Si un utilisateur tente d'acceder à une page protégée sans en avoir les droits
# il sera redirigé sur cette page
login_path: security_login
# Securisation des formulaires
csrf_token_generator: security.csrf.token_manager
# La page par defaut apres une connexion reussie
default_target_path: admin

logout:
# La route où se trouve le process de deconnexion
path: security_logout
# La route sur laquelle doit etre rediriger l'utilisateur apres une deconnexion
target: index

access_control:
# Les regles de securité
# Là dans ce cas seul les utilisateurs ayant le rôle ROLE_ADMIN
# peuvent acceder à toutes les pages commençant par /admin
- { path: '^/admin', roles: ROLE_ADMIN }

11) framework.yml :

framework:
secret: '%env(APP_SECRET)%'
#default_locale: en
csrf_protection: { enabled: true }
#http_method_override: true
# uncomment this entire section to enable sessions
session:
# With this config, PHP's native session handling is used
handler_id: ~
#esi: ~
#fragments: ~
php_errors:
log: true

12) SecurityController.php :

<?php

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;

class SecurityController extends AbstractController
{
/**
* @Route("/login", name="security_login")
*/
public function login(AuthenticationUtils $helper): Response
{
return $this->render('Security/login.html.twig', [
// dernier username saisi (si il y en a un)
'last_username' => $helper->getLastUsername(),
// La derniere erreur de connexion (si il y en a une)
'error' => $helper->getLastAuthenticationError(),
]);
}

/**
* La route pour se deconnecter.
*
* Mais celle ci ne doit jamais être executé car symfony l'interceptera avant.
*
*
* @Route("/logout", name="security_logout")
*/
public function logout(): void
{
throw new \Exception('This should never be reached!');
}
}

13) Et son template donc, dans Security/login.html.twig :

{% extends 'base.html.twig' %}

{% block body %}
{% if error %}
<div class="alert alert-danger">
{{ error.messageKey }}

</div>
{% endif %}
<div class="row">
<div class="col-sm-5">
<div class="well">
<form action="{{ path('security_login') }}" method="post">
<fieldset>
<legend><i class="fa fa-lock" aria-hidden="true"></i> Connexion</legend>
<div class="form-group">
<label for="username">Username</label>
<input type="text" id="username" name="_username" value="{{ last_username }}" class="form-control"/>
</div>
<div class="form-group">
<label for="password">Mot de passe</label>
<input type="password" id="password" name="_password" class="form-control" />
</div>
<input type="hidden" name="_csrf_token" value="{{ csrf_token('authenticate') }}"/>
<button type="submit" class="btn btn-primary">
<i class="fa fa-sign-in" aria-hidden="true"></i> On entre
</button>
</fieldset>
</form>
</div>
</div>

{% endblock %}

À ce stade, la route http://localhost/UserDemo/public/login doit fonctionner.

14) Contrôleur IndexController.php pour la page index et admin

<?php

namespace App\Controller;

use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Response;

class IndexController extends Controller
{
/**
* @Route("/", name="index")
*/
public function index()
{
return $this->render('index.html.twig');
}

/**
* @Route("/admin", name="admin")
*/
public function admin()
{
return $this->render('Admin/index.html.twig');
}
}

15) Avec leurs templates dans templates / index.html.twig et templates / Admin / index.html.twig :

Index

{% extends 'base.html.twig' %}

{% block body %}
<a href="/{{ path('security_login')}}"> Connexion</a><br>
<a href="/{{ path('user_registration')}}"> Inscription</a>

{% endblock %}

Admin

{% extends 'base.html.twig' %}

{% block body %}
Bienvenue {{ app.user.username }} !

{% endblock %}

16) RegistrationController.php

<?php
// src/Controller/RegistrationController.php
namespace App\Controller;

use App\Form\UserType;
use App\Entity\User;
use App\Events;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\EventDispatcher\GenericEvent;

class RegistrationController extends Controller
{
/**
* @Route("/register", name="user_registration")
*/
public function registerAction(Request $request, UserPasswordEncoderInterface $passwordEncoder, EventDispatcherInterface $eventDispatcher)
{
$user = new User();
$form = $this->createForm(UserType::class, $user);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$password = $passwordEncoder->encodePassword($user, $user->getPassword());
$user->setPassword($password);
// Par defaut l'utilisateur aura toujours le rôle ROLE_USER
$user->setRoles(['ROLE_USER']);
// On enregistre l'utilisateur dans la base
$em = $this->getDoctrine()->getManager();
$em->persist($user);
$em->flush();
//On déclenche l'event
$event = new GenericEvent($user);
$eventDispatcher->dispatch(Events::USER_REGISTERED, $event);
return $this->redirectToRoute('security_login');
}
return $this->render(
'register.html.twig',
array('form' => $form->createView())
);
}
}

17) Le form de registration

<?php
// /src/Form/UserType.php
namespace App\Form;

use App\Entity\User;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\Extension\Core\Type\EmailType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Extension\Core\Type\RepeatedType;
use Symfony\Component\Form\Extension\Core\Type\PasswordType;

class UserType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('fullName', TextType::class)
->add('email', EmailType::class)
->add('username', TextType::class)
->add('password', RepeatedType::class, array(
'type' => PasswordType::class,
'first_options' => array('label' => 'Password'),
'second_options' => array('label' => 'Repeat Password'),
))
;
}

public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => User::class,
));
}
}

Et son template register.html.twig :

{# templates/register.html.twig #}
{% extends 'base.html.twig' %}

{% block body %}

{{ form_start(form) }}
{{ form_row(form.fullName) }}
{{ form_row(form.username) }}
{{ form_row(form.email) }}
{{ form_row(form.password.first) }}
{{ form_row(form.password.second) }}

<button type="submit">S'inscrire !</button>
{{ form_end(form) }}

{% endblock %}

À ce stade, la route http://localhost/UserDemo/public/register doit fonctionner.


Sonata

1) Créez un nouveau projet de test

Nommé ici test-sonata :

composer create-project symfony/skeleton test-sonata

2) Installez l'Admin-bundle

cd test-sonata
composer require symfony/apache-pack
composer require sonata-project/admin-bundle

Et hop une belle admin Sonata :

http://localhost/test-sonata/public/admin/dashboard

Vous pouvez modifier son title dans config / packages / sonata_admin.yaml.

3) Doctrine

composer require symfony/orm-pack
composer require symfony/maker-bundle --dev
composer require sonata-project/doctrine-orm-admin-bundle

4) La base de données

Dans .env :

DATABASE_URL=mysql://root:''@127.0.0.1:3306/test-sonata
php bin/console doctrine:database:create

5) Entité Customer

php bin/console make:entity

Créez les champs lastname, firstname et email1. String, 255 carcatères et non-null (Enter pour validé par défaut).

php bin/console make:migration
php bin/console doctrine:migrations:migrate

6) La classe de gestion de l'entité :

Dans src / admin, créez le fichier CustomerAdmin.php :

<?php

namespace App\Admin;

use Sonata\AdminBundle\Admin\AbstractAdmin;
use Sonata\AdminBundle\Datagrid\ListMapper;
use Sonata\AdminBundle\Datagrid\DatagridMapper;
use Sonata\AdminBundle\Form\FormMapper;
use Symfony\Component\Form\Extension\Core\Type\TextType;

class CustomerAdmin extends AbstractAdmin
{
protected function configureFormFields(FormMapper $formMapper)
{
$formMapper->add('lastname', TextType::class);
$formMapper->add('firstname', TextType::class);
$formMapper->add('email1', TextType::class);
}

protected function configureDatagridFilters(DatagridMapper $datagridMapper)
{
$datagridMapper->add('lastname');
$datagridMapper->add('firstname');
$datagridMapper->add('email1');
}

protected function configureListFields(ListMapper $listMapper)
{
$listMapper->addIdentifier('lastname');
$listMapper->addIdentifier('firstname');
$listMapper->addIdentifier('email1');
}
}

Puis dans config / services.yaml :

     admin.customer:
class: App\Admin\CustomerAdmin
arguments: [~, App\Entity\Customer, ~]
tags:
- { name: sonata.admin, manager_type: orm, label: Customer }
public: true

Et dans config / packages / framework.yaml :

framework:
    translator: { fallbacks: ["%locale%"] }

Attention aux tabulations ! Que des espaces !

Si besoin :

composer require templating

À ce stade vous avez un CRUD sur votre entité Customer.

8) Diviser un formulaire en plusieurs onglets

Au moment d'appeler vos champs, ajouter une tab et un with avec quelques paramètres. Exemple si vous aviez ce code suivant :

$formMapper->add('city', TextType::class);
$formMapper->add('state', TextType::class);
$formMapper->add('country', TextType::class);

Il deviendra :

$formMapper->tab('General2') // the tab call is optional
->with('Addresses', [
'class' => 'col-md-5',
'box_class' => 'box box-solid box-danger',
'description' => 'Lorem ipsum',
])
->add('city', TextType::class)
->add('state', TextType::class)
->add('country', TextType::class)
->end() ->end();

XXX) [WORKING, ne fonctionne pas au 18/07/2018] Users

composer require sonata-project/user-bundle
composer require swiftmailer-bundle

Créez config / packages / fos_user.yaml :

fos_user:
db_driver: orm
firewall_name: main
user_class: App\Entity\User
from_email:
address: "Cette adresse e-mail est protégée contre les robots spammeurs. Vous devez activer le JavaScript pour la visualiser."
sender_name: "Cette adresse e-mail est protégée contre les robots spammeurs. Vous devez activer le JavaScript pour la visualiser."
composer require jms/serializer-bundle

Package SonataAdmin et FosUser pour Symfony 4

Au 18 juillet 2018, il n'était pas simple d'utiliser Sonata et Fos sur SF4, mais j'ai trouvé un package sur Git, très fonctionnel. Merci Leonan Luppi.

Téléchargez le package suivant :

https://github.com/schoolofnetcom/symfony4-avan-ando-com-sonata-admin

Puis :

composer update

Sans doute aussi :

composer require sonata-project/notification-bundle
composer require symfony/apache-pack

Dans config / services.yaml :

locale: 'pt_BR'
devient
locale: 'en_EN'
ou
locale: 'fr_FR'

Pour changer le nom de l'appli et le logo, dans config / packages / sonata_admin.yaml :

title: 'My tool'
title_logo: img/admin/my_logo.png

Dans .env :

DATABASE_URL=mysql://root:''@127.0.0.1:3306/scrud

J'ai remarqué au parfois mieux vaut faire :

DATABASE_URL=mysql://root:''@localhost:3306/scrud
php bin/console doctrine:database:create
php bin/console make:migration
php bin/console doctrine:migrations:migrate
php bin/console fos:user:create adminuser --super-admin
composer require symfony/orm-pack

Construction d'API

1) Création d'un projet

composer create-project symfony/skeleton mon-API

2) Installer Apache, Annotation, JMS-serializer, Doctrine et Maker

cd mon-API
composer require symfony/apache-pack
composer require annotations
composer require jms/serializer-bundle
composer require symfony/orm-pack
composer require doctrine maker

3) Base de données

Dans .env :

DATABASE_URL=mysql://root:''@127.0.0.1:3306/mon_api
php bin/console doctrine:database:create

4) Entité de test

php bin/console make:entity Article

Avec son 1er champ title, en string, 100 caractères, non-nullable ; et un 2ème champ content, en text, non-nullable.

php bin/console make:migration
php bin/console doctrine:migrations:migrate

5) Le contrôleur

Dans src / Controller créer un fichier de type MonApi.php :

php bin/console make:controller MonApi

Puis dans le fichier MonApiController.php :

<?php

namespace App\Controller;

use Symfony\Component\Routing\Annotation\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;

use App\Entity\Article;
use Symfony\Component\HttpFoundation\Response;

class MonApiController extends Controller
{
/**
* @Route("/articles/{id}", name="article_show")
*/
public function showAction()
{
$article = new Article();
$article
->setTitle('Mon premier article')
->setContent('Le contenu de mon article.')
;
$data = $this->get('jms_serializer')->serialize($article, 'json');

$response = new Response($data);
$response->headers->set('Content-Type', 'application/json');

return $response;
}
}

L'URL http://localhost/mon-API/public/articles/1 doit renvoyer un json.

6) Postman

Installez l'extension Postman pour Chrome, et testez une requête GET sur l'URL http://localhost/mon-API/public/articles/1.

Postman doit bien renvoyer le même json, tel que décrit dans votre contrôleur, et visible dans un navigateur.

7) Requête Post

Modifions maintenant la classe de notre contrôleur afin d'être capable d'envoyer des données soumises par un cient :

...

use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use Symfony\Component\HttpFoundation\Request;

...

/**
* @Route("/articles", name="article_create")
* @Method({"POST"})
*/
public function createAction(Request $request)
{
$data = $request->getContent();
$article = $this->get('jms_serializer')->deserialize($data, 'App\Entity\Article', 'json');
$em = $this->getDoctrine()->getManager();
$em->persist($article);
$em->flush();
return new Response('', Response::HTTP_CREATED);
}
...

Maintenant la route http://localhost/mon-API/public/articles attend des données en fonction de l'entité Article, et sous forme de json.

L'URL elle-même ne fonctionne pas telle quelle, mais sous Postman, une requête de type Post avec des données json remplira bien la base de données.

postman 1

8) Méthodes d'affichage

Modifions notre contrôleur afin d'afficher de vrais articles selon l'id ainsi que la liste complète des articles :

<?php

namespace App\Controller;

use Symfony\Component\Routing\Annotation\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;

use App\Entity\Article;
use Symfony\Component\HttpFoundation\Response;

use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use Symfony\Component\HttpFoundation\Request;

class MonApiController extends Controller
{
/**
* @Route("/articles/{id}", name="article_show")
*/
public function showAction(Article $article)
{
$data = $this->get('jms_serializer')->serialize($article, 'json');

$response = new Response($data);
$response->headers->set('Content-Type', 'application/json');

return $response;
}

/**
* @Route("/articles", name="article_create")
* @Method({"POST"})
*/
public function createAction(Request $request)
{
$data = $request->getContent();
$article = $this->get('jms_serializer')->deserialize($data, 'App\Entity\Article', 'json');

$em = $this->getDoctrine()->getManager();
$em->persist($article);
$em->flush();

return new Response('', Response::HTTP_CREATED);
}

/**
* @Route("/articles_list", name="article_list")
* @Method({"GET"})
*/
public function listAction()
{
$articles = $this->getDoctrine()->getRepository('App:Article')->findAll();
$data = $this->get('jms_serializer')->serialize($articles, 'json');

$response = new Response($data);
$response->headers->set('Content-Type', 'application/json');

return $response;
}
}

9) Cacher des champs

Il faut modifier l'entité pour appeler le Serializer (use JMS\Serializer\Annotation as Serializer;), cacher tous les champs (* @Serializer\ExclusionPolicy("ALL")), puis en appeler certains (* @Serializer\Expose) :

<?php

namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;
use JMS\Serializer\Annotation as Serializer;

/**
* @ORM\Entity(repositoryClass="App\Repository\ArticleRepository")
* @Serializer\ExclusionPolicy("ALL")
*/
class Article
{
/**
* @ORM\Id()
* @ORM\GeneratedValue()
* @ORM\Column(type="integer")
*/
private $id;

/**
* @ORM\Column(type="string", length=100)
* @Serializer\Expose
*/
private $title;

/**
* @ORM\Column(type="text")
* @Serializer\Expose
*/
private $content;
...

xx

xx

xx


Tests PhpUnit

Si phpunit-bridge, browser-kit et css-selector sont installés, on peut créer des tests fonctionnels (merci à dev-web.io).

1) Créez un premier test vide :

php bin/console make:functional-test

2) Décommentez la mention des sessions dans config / packages / test / framework.yaml :

framework:
test: ~
# Uncomment this section if you're using sessions
session:
storage_id: session.storage.mock_file

3) Ajoutez cette ligne dans le fichier phpunit.xml.dist :

Vous pouvez utiliser la même base de données ou une autre dédiée.

 

<!-- define your env variables for the test env here -->
<env name="DATABASE_URL" value="mysql://root:@127.0.0.1/userDemo" />

4) Modifiez ensuite la fonction par défaut de votre contrôleur de test.

Nous reprenons ici l'exemple précédent de l'annuaire utilisateur, et vérifions qu’un message d’erreur s’affiche bien lorsque l’utilisateur s’inscrit en ne saisissant pas les 2 mêmes mot de passe :

public function testCheckPassword(){
$client = static::createClient();

$crawler = $client->request(
'GET',
'/register'
);

$form = $crawler->selectButton('S\'inscrire')->form();

$form['user[email]'] = Cette adresse e-mail est protégée contre les robots spammeurs. Vous devez activer le JavaScript pour la visualiser.';
$form['user[username]'] = 'usernametest';
$form['user[fullName]'] = 'John Doe';
$form['user[password][first]'] = 'pass1';
$form['user[password][second]'] = 'pass2';

$crawler = $client->submit($form);

//echo $client->getResponse()->getContent();

$this->assertEquals(1,
$crawler->filter('li:contains("This value is not valid.")')->count()
);
}

5) Lancez ensuite le test :

php bin/phpunit

Quelques commandes

Voir les routes

En vous plaçant d'abord dans...

php bin/console debug:router

Voir les commandes disponibles

php bin/console

Créer un contrôleur

php bin/console make:controller ...

Créer la base de données

Dans le fichier .env, personnaliser la ligne DATABASE_URL de cette façon par exemple (root est l'utilisateur de base de données, et les guillemets sont le mot de passe vide) :

DATABASE_URL=mysql://root:''@127.0.0.1:3306/ma_jolie_base

Puis :

php bin/console doctrine:database:create

Qui aura pour effet de créer la base de données dans MySQL.

Migration (shéma et tables BDD)

php bin/console doctrine:migrations:diff

Puis :

php bin/console doctrine:migrations:migrate

Ou :

php bin/console make:migration

Et :

php bin/console doctrine:migrations:migrate

Forcer la mise à jour de la BDD

En cas d'erreur bloquant la migration normale :

php bin/console doctrine:schema:update --force

Créer une entité

php bin/console make:entity ...

Connaître sa version de Symfony

php bin/console --version

Générer un CRUD sur une entité

php bin/console make:crud ...

Mettre à jour composer

Dans votre projet :

composer update

Interroger la BDD depuis le CLI

php bin/console doctrine:query:sql "SELECT * FROM ..."

Lancer le serveur local

Depuis votre projet :

php -S 127.0.0.1:8000 -t public

Puis http://localhost:8000/

Vérifier sa version de Symfony

php bin/console --version

Lancer le serveur web

Si web-server-bundle est installé : 

php bin/console server:run

Liens