Il est possible également de vérifier le Host avec checkHost=true.
Cette contrainte vérifie qu’il existe un champ A, un champ AAAA ou un
champ MX. Mais je ne recommande pas l’utilisation de cette contrainte
car checkMX permet de s’assurer qu’il existe au moins un champ MX et
c’est ce qui est important pour envoyer un mail.
Activer la validation de l’email selon la RFC
La RFC 3696 définit précisément le format d’une adresse mail.
Il faut au préalable ajouter le bundle suivant : egulias/email-validator. Ce bundle permet de vérifier le format de l’adresse mail selon les préconisations de le RFC 3696.
composer require egulias/email-validator
Double validation
Avec ces deux contraintes, la validation est très correcte. On est sûr que le domaine existe et que l’adresse est au bon format. Pour utiliser les deux contraintes en même temps :
Si tu veux personnaliser les messages d’erreur en fonction du type de contrainte :
/**
* @var string
* @ORM\Column(name="email", type="string", length=255)
* @Assert\Email(strict=true, message="Le format de l'email est incorrect")
* @Assert\Email(checkMX=true, message="Aucun serveur mail n'a été trouvé pour ce domaine")
*/
private $email;
Comment être sûr à 100% que l’adresse mail saisie est bien réelle ?
Envoyer un mail sur l’adresse mail saisie contenant un lien protégé qui, lorsqu’il est appelé, valide l’adresse mail
Envoyer un mail sur l’adresse mail saisie en indiquant un ReturnPath. Si l’adresse n’existe pas, tu recevra un mail de no-delivery sur cette adresse. Il faudra alors déclencher une opération à la réception de l’email pour invalider l’adresse mail.
Inconvénient : On ne peut pas vérifier au moment de la saisie.
Nous avons des utilisateurs à qui il est possible d’ajouter des diplômes.
Notre entité User :
Entity/User.php :
class User
{
/**
* @var integer
*
* @ORM\Column(name="id", type="integer")
* @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
// ... Autres attributs : nom / prénom ...
/**
* @var Diplome
*
* @ORM\OneToMany(targetEntity="Diplome", mappedBy="user", cascade="all", orphanRemoval=true)
* @Assert\Valid()
* @OrderBy({"position" = "ASC"})
*/
private $diplomes;
/**
* Constructor
*/
public function __construct()
{
$this->diplomes = new ArrayCollection();
}
/**
* Get id
*
* @return integer
*/
public function getId()
{
return $this->id;
}
// ... Autres getter et setter
/**
* Add diplome
*
* @param Diplome $diplome
*
* @return User
*/
public function addDiplome(Diplome $diplome)
{
$this->diplomes[] = $diplome;
$diplome->setUser($this);
return $this;
}
/**
* Remove diplome
*
* @param Diplome $diplome
*/
public function removeDiplome(Diplome $diplome)
{
$this->diplomes->removeElement($diplome);
}
/**
* Get diplomes
*
* @return \Doctrine\Common\Collections\Collection
*/
public function getDiplomes()
{
return $this->diplomes;
}
}
On passe l’attribut orphanRemoval à true pour la relation OneToMany avec Diplome. OrphanRemoval va permettre de supprimer une entité Diplome lorsqu’elle est retirée de la collection de l’entité User. On indique aussi l’attribut cascade = all. Ceci permet qu’un évènement doctrine sur l’entité User déclanche en cascade le même évènement sur l’entité Diplome : on persite un utilisateur donc on persiste ses diplômes, on supprime un utilisateur donc on supprime ses diplômes.
Dans la fonction addDiplome, on ajoute la ligne :
$diplome->setUser($this);
On ajoute le diplôme à l’utilisateur, on doit alors informer le dipôme de l’utilisateur auquel il est lié.
Voici maintenant notre entité Diplome :
Entity/Diplome.php :
class Diplome
{
/**
* @var integer
*
* @ORM\Column(name="id", type="integer")
* @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* @var string
*
* @ORM\Column(name="name", type="string", length=255)
*
* @Assert\NotBlank()
* @Assert\Length(max="255")
*/
private $name;
/**
* @var User
*
* @ORM\ManyToOne(targetEntity="User", inversedBy="diplomes")
* @ORM\JoinColumn(nullable=false)
* @Assert\NotNull()
*/
private $user;
/**
* Constructor
*
* @param User $user
*/
public function __construct(User $user = null)
{
$this->user = $user;
}
/**
* Get id
*
* @return integer
*/
public function getId()
{
return $this->id;
}
/**
* Set user
*
* @param User $user
*
* @return Diplome
*/
public function setUser(User $user)
{
$this->user = $user;
return $this;
}
/**
* Get user
*
* @return User
*/
public function getUser()
{
return $this->user;
}
}
Notre formulaire qui va permettre d’ajouter un utilisateur avec ses diplômes :
Form/UserType.php :
$builder
// ->add('nom', TextType::class)
// Autres champs de l'entité User ...
->add('diplomes', CollectionType:class, array(
'type' => DiplomeType::class,
'allow_add' => true,
'allow_delete' => true,
'prototype' => true,
));
On ajoute un champ diplome de type Collection :
type : le type de champ de la collection, ici ce sera un autre formulaire de type diplome
allow_add : On veut pouvoir en ajouter
allow_delete : On veut pouvoir en supprimer
prototype : On veut qu’un prototype soit défini afin de pouvoir gérer la collection en javascript côté client.
Et là quand j’envoie mon formulaire après avoir ajouté un diplome, j’ai la fameuse erreur :
Cette valeur ne doit pas être nulle.
Pour info, j’ai cette erreur parce que j’ai indiqué @Assert\NotNull() pour l’attribut user de l’entité Diplome, sans cette Assert, j’aurai eu une belle erreur Symfony : Intégrity constraint violation
Apparement, la fonction addDiplome de mon entité User n’est pas appelée …
Pour contrer cette erreur, on retrouve des codes du style :
Form/UserType.php :
$builder->addEventListener(
FormEvents::SUBMIT,
function (FormEvent $event) {
foreach ($event->getData()->getDiplomes() as $diplome) {
$diplome->setUser($event->getData());
}
}
);
Attention, ce code n’est pas recommandé, utilises plutôt la solution décrite plus bas.
Explication
: Au submit, on fait une boucle sur les diplômes de la collection et,
pour chacun, on appelle la méthode setUser qui va les lier à notre
object User.
C’est pas génial comme code … (Et j’ai vu pire aussi ! Parfois c’est dans le contrôler qu’on peut trouver cette boucle sur les catégories).
Comment bien gérer cette collection ? L’attribut by_reference
Il existe un attribut by_reference qu’il est indiqué pour tout type de formulaire. Par défaut, cet attribut est passé à true.
En passant cet attribut à false, on force Symfony à appeler le setter de l’entité.
Dans le cas d’une collection, on force Symfony à appeler les méthodes add et remove de l’attribut.
Pour notre exemple, on rajoute by_reference = false.
Maintenant, au submit, la méthode
addDiplome de l’entité User sera appelée et ainsi la ligne suivante sera
exécutée, permettant de lier le diplôme à l’utilisateur :
Tout va bien jusque là, mais pour ajouter une image, ça devient vite compliqué si elle est en local par exemple. On doit l’envoyer sur un serveur, trouver son lien direct, le copier, etc …
Pour palier à ce problème, nous allons utiliser un bundle qui va intégrer tout seul un gestionnaire de média à l’interface CKEditor. Ainsi tu pourra ajouter, modifier, supprimer les images, vidéos, pdfs, etc … directement depuis l’interface.
Exemple :
Installation de FMElfinderBundle
composer.json
{
"require": {
"helios-ag/fm-elfinder-bundle": "6.*",
}
"config": {
"component-dir": "web/dossier_de_mon_choix" # Les ressources utilisés par fm-elfinder seront placées dans ce dossier
}
}
app/AppKernel.php
$bundles = array(
// ...
new FM\ElfinderBundle\FMElfinderBundle(),
);
C’est tout ! Il ne reste plus qu’à essayer. Cliques sur ajouter une image et sur Explorer le serveur. N’oublies par de créer le répertoire ‘your_path_in_public_directory’ dans le répertoire web de votre application sinon CKEditor ne pourra pas envoyer les images et tu aura une alerte indiquant que votre configuration est invalide.
Utiliser le BBcode
Utiliser le BBcode permet d’éviter d’avoir du code html dans votre base de donnée. C’est plus ou moins une bonne pratique. Un code en BBcode ressemble à :
Pour afficher le BBcode, il va falloir le convertir en HTML. Pour celà, un bundle Symfony existe : FMBBCodeBundle.
Regardes la documentation du bundle pour l’installer et le configurer.Attention, il faut utiliser la version 7.* pour Symfony 3 et non la version 6 comme préconisée dans la documentation du bundle
Ce bundle ajoute un filtre twig qui te permet de rendre le BBcode. Exemple :
{{ content | bbcode_filter('default') }}
Et pour que le HTML soit interprété :
{{ content | bbcode_filter('default') | raw }}
Utiliser un colorateur syntaxique de code intégré à CKEditor
Nous allons voir ici comment utiliser un colorateur de code. J’utilise sur cette page la bibliothèque highligh.js pour colorer le code.
Commençons par installer le plugin pbckcode dans votre fichier config.yml :
Ok mais cette fonction permet de vérifier le rôle de l’utilisateur actuellement connecté qui fait la demande.
Maintenant, comment faire pour vérifier qu’un autre utilisateur a bien le ROLE_ADMIN ? (Exemple, afficher une liste d’utilisateurs en affichant si oui ou non il est admin).
On pense tout de suite à :
if ($user->hasRole('ROLE_ADMIN'))
Mais un utilisateur SUPER_ADMIN retournerai « false » à cette fonction car la fonction hasRole ne vérifie pas la hiérarchie des rôles.
C’est le même problème si l’on veut vérifier que l’utilisateur a bien le ROLE_USER alors qu’il est admin ou client.
Résolution du problème
On peut utiliser un service présent dans le core de Symfony dans un controller :
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface;
class UserController extends AbstractController {
/**
* @Route("/{user}", name="app_user_view", requirements={"user"="\d+"})
*/
public function view(AccessDecisionManagerInterface $accessDecisionManager, User $user): Response {
$token = new UsernamePasswordToken($user, 'none', 'none', $user->getRoles());
if ($accessDecisionManager->decide($token, 'ROLE_ADMIN')) {
// L'utilisateur $user a le rôle ROLE_ADMIN
}
...
Le service AccessDecisionManager permet de vérifier si l’utilisateur a bien les droits tout en vérifiant la hiérarchie des roles. Ce service permet aussi de vérifier les droits selon des règles spécifiques définies par les Voter.
Créer un service dédié à la vérification des droits
<?php
namespace App\Services;
use App\Entity\User;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface;
class GrantedService
{
private $accessDecisionManager;
/**
* Constructor
*
* @param AccessDecisionManagerInterface $accessDecisionManager
*/
public function __construct(AccessDecisionManagerInterface $accessDecisionManager) {
$this->accessDecisionManager = $accessDecisionManager;
}
public function isGranted(User $user, $attributes, $object = null) {
if (!is_array($attributes))
$attributes = [$attributes];
$token = new UsernamePasswordToken($user, 'none', 'none', $user->getRoles());
return ($this->accessDecisionManager->decide($token, $attributes, $object));
}
}
C’est tout, cette fonction fonctionne maintenant exactement comme la fonction isGranted disponible de base dans les controller sauf qu’il faut spécifier l’utilisateur à checker :
class UserController extends AbstractController {
/**
* @Route("/{user}", name="app_user_view", requirements={"user"="\d+"})
*/
public function view(GrantedService $grantedService, User $user): Response {
if ($grantedService->isGranted($user 'ROLE_ADMIN')) {
// L'utilisateur $user a le rôle ROLE_ADMIN
}
...
C’est tout pour aujourd’hui ! N’hésitez pas à poser vos questions dans les commentaires et à partager cet article ! Merci
Les logs te permette de garder une trace de ce qui se passe sur ton application. Souvent ils se révèlent une source très précieuse d’informations lorsqu’un utilisateur soulève un bug ou un comportement anormal.
De plus, avec Symfony, tu peux mettre en place des mécanismes simples pour déclencher des actions en fonction du niveau d’alerte des logs, par exemple pour recevoir un mail lorsqu’un problème survient. N’hésites pas à logger le plus d’informations possible, Symfony te permet de gérer simplement la rotation des fichiers de log pour ne pas perdre indéfiniment en espace disque.
Niveaux de logs
Comme beaucoup de systèmes de log, Monolog utilise plusieurs niveaux. Par ordre croissant, du moins alertant au plus critique des logs :
DEBUG : Utilisé en général pour développer ou débugger une application afin de vérifier une valeur ou un bon déroulement.
INFO : Information sur un événement commun et normal (exemple : un utilisateur qui se connecte).
NOTICE : Comportement normal signifiant mais pas d’erreur.
WARNING : Événement exceptionnel mais sans erreur (exemple : Utilisation d’une fonction dépréciée).
ERROR :Erreur d’exécution qui ne demande pas d’intervention immédiate mais qui doit être enregistrée. (exemple : une erreur 404, un objet non trouvé en base avec tel identifiant …).
CRITICAL : Exception inattendue soulevée pendant l’exécution de l’application. Cette action est généralement accompagnée d’alerte mail. (exemple : un paramètre manquant dans la configuration d’un module).
ALERT : « Alerte rouge », tout le service ou sa base de données est indisponible. Cette action est généralement accompagnée d’alerte sms et / ou d’alerte monitoring sonore. (exemple : le site est inaccessible par votre outil de monitoring).
EMERGENCY : Le système est inutilisable, tout est complètement cassé et nécessite une grosse intervention pour tout remettre d’aplomb. Des données sont perdues / corrompues. Bref … Je vous laisse imaginer la catastrophe que ça peut être. Je vous souhaite de ne jamais voir apparaître ce genre de log ! (exemple : détection d’un hacking bien hard de votre site).
Logger avec Symfony
Pour utiliser le système de log de Symfony :
Dans un contrôler
public function index(Logger $logger): Response {
$logger->info('Tout va bien');
$logger->error('Je ne peux pas trouver la voiture n°53');
$logger->critical('Ca ne marche pas !!');
}
Dans un service
namespace App\Mailer;
use Monolog\Logger;
class Mailer
{
protected $logger;
public function __construct(Logger $logger) {
$this->logger = $logger;
}
public function faireQuelqueChose() {
$this->logger->info('Je fais quelque chose');
$this->logger->critical('Mais je n\'ai pas réussi ...');
}
}
Configuration du logger
La configuration par défaut que l’on trouve pour un environnement de production ressemble à celle-ci :
Ici main, nested,console sont appelés des handlers (des gestionnaires), le nom donné est arbitraire. Pour chaque handler, on définit un type. Nous verrons les différents types de handler possible plus bas.
Chaque handler est ensuite appelé dans l’ordre défini (Attention, on a parfois des handlers imbriqués. Ils ne sont pas appelés par défaut ! C’est le cas ici puisque nested est imbriqué dans main).
Pour expliquer cette configuration :
On a ici un FingersCrossedHandler (handler qui en déclenche un autre, ici il s’agit de main) qui se déclenche seulement lorsque le niveau de log attendu est atteint (ici on attend un log de type error). Ce handler, une fois déclenché, appelle le handler nested. Nested est de type stream (handler qui écrit les logs) qui va écrire les logs dans un fichier à partir d’un level défini (ici tout les logs plus importants ou égaux à debug).
Le handler Nested n’est pas déclenché par défaut car il est imbriqué dans main.
Le handler console est déclenché quant à lui pour tous les logs, nous ne verrons pas ce handler dans cet article, si vous désirez en savoir plus, lisez cet article du blog de Symfony.
Différents types de handles
Il existe plusieurs types de handler avec chacun une fonctionnalité précise :
finders_crossed : Ce handler stocke dans un buffer tout les logs qui passe. Lorsqu’un des logs dépasse le niveau minimum requis, il appelle un autre handler avec tous les logs contenus dans son buffer.
stream : Ce handler écrit le log qu’il reçoit dans un fichier si son niveau dépasse le niveau minimum requis.
rotating_file : Ce handler fait la même chose que stream mais fait une rotation des fichiers pour effacer les logs anciens.
group : Ce handler envoit le log reçu à plusieurs handles (exemple : pour écrire le log ET l’envoyer par mail)
buffer : Ce handler stocke dans un buffer tout les logs qu’il reçoit puis envoit le buffer à un handler à la fin de l’exécution de la requête.
swit_mailler : Ce handle envoit par mail les logs (souvent passé par un handler de type buffer)
console : Ce handler permet de définir les niveaux d’affichage de log dans la console.
Nous avons vu avec l’exemple par défaut comment marche les handler finder_crossed et stream.
Nous verrons dans les exemples ci-dessous comment sont utilisés les handlers pour faire ce que l’on veut.
Envoyer les alertes par mail
monolog:
handlers:
mail:
type: fingers_crossed
action_level: critical
handler: buffered
buffered:
type: buffer
handler: swift
swift:
type: swift_mailer
from_email: contact@domaine.com
to_email: error@domaine.com
subject: Une erreur critique est survenue
level: info
Ici on attend un log de niveau critical pour déclencher le handle buffered. Une fois déclenché, le handler buffered va stocker tout les logs et les passer à la fin de l’éxécution de la requête du client au handler swift. Ce dernier va envoyer un mail en triant les logs reçus et en ne gardant que ceux de niveau minimum info.
Contrairement à un handler de type finger_crossed, un handler de type buffer appelle un handler une seule fois avec le contenu de son buffer alors que finger_crossed appelle un autre handler pour chaque log qu’il rencontre.
Rotation des logs
Pour faire la rotation des logs, on va utiliser simplement le handler rotating_file au lieu de steam :
Ici on écrit tous les logs de niveau supérieur à debug dans un fichier en rajoutant la date du jour dans le nom du fichier. Au bout de 10 fichiers créés, le plus vieux est supprimé automatiquement dès qu’un nouveau est créé et ainsi de suite. On a donc au minimum 10 jours de logs derrière nous. Vous pouvez augmenter ce paramètre avec max_files.
Les channels
Les logs utilisent des channels pour s’identifier. Par exemple, les logs de doctrine sont sur le channel « doctrine » et ceux sur les authentifications sont sur le channel « security ».
Ainsi on peux lancer des handlers différents en fonction du type de channel.
Ici on écrit tous les logs qui ne viennent pas de doctrine, ni de security dans un fichier symfony.log (car le type de handler est stream). On écrit tous les logs de doctrine dans doctrine.log et tout ceux de security dans auth.log.
Vos propres channels
Vous pouvez bien évidemment créer vous aussi vos propres channel pour logger vos log comme vous le souhaitez. Pour cela, ajouter un tag monolog.logger dans vos services :
Et voilà ! Dès qu’un log critical est lu, le handler main_criticale appelle le handler grouped. Le handler grouped qui est de type group va envoyer chaque log vers le handler streamed et vers le handler buffered en même temps. Ces deux handler vont ensuite remplir leurs fonctions : l’un écrire avec rotation de fichier et l’autre stocker dans un buffer avant de l’envoyer au handler qui envoit un mail.
Un exemple complet
Voici un exemple que j’utilise généralement sur mes environnements de prod :
Le handler main sera déclenché pour tous les logs de niveau supérieur ou égal à info et il écrira à chaque fois le log dans un fichier dans app/logs/prod.all-2015-01-05.log (avec la date du jour).
Le handler login fera la même chose mais seulement pour le channel security (les authentifications) et stocke le tout dans un fichier prod.auth-2015-01-05.log
Les logs de niveau error déclencheront le handler main_error qui appellera streamed_error qui va écrire tout les logs du buffer dans un fichier prod.error-2015-01-05.log
Les logs de niveau critical déclencherons le handler main_critical qui va à la fois écrire tout les logs dans un fichier prod.critical-2015-01-05.log et à la fois envoyer un mail avec le buffer pour prévenir de l’erreur survenue (les deux handlers sont déclenchés par le handler group : grouped_critical).
C’est fini ! N’hésitez pas à poser vos questions dans les commentaires et à partager cet article ! Merci