Commande pour créer un nouveau
composer create-project symfony\website-skeleton NomDuProjet
Pour installer les bibliothèques PHP externes entrez cette commande
composer install
Configurez l'accés à la base de données dans le fichier .env Ensuite mettre à jour la base de donnée avec la commande
php bin/console doctrine:schema:update --force
Webpack permet de condenser tous les fichiers assets dans un seul Ex tous les fichier js sont minifiés et placés dans un seul fichier Encore est un bundle Sf pour féciliter l'installation de Webpack
Pour installer les modules
npm install --save-dev
Pour générer les fichier CSS et JS entrez la commande
npm run dev
Pour générer les fichier en production
npm run build
Pour observer les changements en dev
npm run watch
assets: Fichier js/scss
bin: Fichiers binaires tel que la console
config: Fichiers de configuration des modules (Sf < 4: un seul fichier config.yml)
public: Contient l'index.php et les fichiers statiques créés par WebPack
src: Tout le code source de l'application
templates: Contient toutes les vues (fichiers Twig) (Sf < 4: les vues sont dans le dossier Resource/Views des Bundle)
tests: Fichiers pour les tests unitaires
translations: Fichiers de traduction
var: Contient le cache et les fichiers log
vendor: Bibliothèques externes
Afficher les routes
php bin/console debug:router
Les annotations sont des instructions définies dans un commentaire doc (/** */), elles permettent de définir des paramètres rapidement sans aller dans les fichiers config Par exemple pour définir les routes dans un controller
/**
* @Route("/chemin/de/la/route")
*/
Avec paramètres
/**
* @Route("/edit/{id}", name="edit", requirements={"id":"\d+"})
*/
Paramètres par défaut
/**
* @Route("/{page}", name="index", requirements={"page": "\d+"}, defaults={"page" = 1})
* /
Si "page" n'est pas renseignée, elle sera à 1
Définir les méthodes autorisées
/**
* @Route("/new", methods={"GET", "POST"})
* /
Une entité est un objet qui représente une table de la base de données
Définir l'entité, annotation à mettre au dessus de la déclaration de classe
/**
* @ORM\Entity(repositoryClass="Namespace\De\La\Classe")
* @ORM\Table(name="nom_de_la_table")
*/
Définir une colonne
/**
* @ORM\Column(name="nom_du_champ", type="string|text|integer|float|datetime|json_array", nullable=true, length=255)
*/
Il est possible d'indiquer à Doctrine d'appeler des méthodes en fonction de ce qui est fait de l'entité
Par exemple appeler une méthode avant le persist
Il faut ajouter l'annotation @ORM\HasLifecycleCallbacks
suivant en dessous de @ORM\entity
/**
* @ORM\Entity(repositoryClass="App\Repository\ImageRepository")
* @ORM\HasLifecycleCallbacks
*/
Ainsi la méthode suivante est appeler avant un persist
/**
* @ORM\PrePersist()
*/
public function prePersist()
{
}
Il y a d'autres événements tel que preRemove
ou preUpdate
Des annotations permettent de définir des relations entres objets
Exemple: Une seule Image pour un seul Article
/**
* @ORM\OneToOne(targetEntity="App\Entity\Image", cascade={"all"}, orphanRemoval=true)
* @var ?\App\Entity\Image
*/
private $image;
*L'attribut "cascade" parmet d'enregistrer l'image en même temps que l'article (ainsi que la suppression) *L'attribut "orphanRemoval" indique que si l'attribut est null, Doctrine doit supprimer l'image associée s'il y en a une
Plusieurs objets peuvent être associés à un seul autre
/**
* @ORM\ManyToOne(targetEntity="Category", inversedBy="articles")
*/
Pour une relation inverse (ex: obtenir les articles d'une catégorie)
/**
* @ORM\OneToMany(targetEntity"Article", mappedBy="category")
*/
Exemple: plusieurs articles dans plusieurs catégories Ainsi Doctrine va créer une table intermediaire qui sera complétement transparente dans notre appli
/**
* @ORM\ManyToMany(targetEntity="App\Entity\Category", inversedBy="articles")
* @var ?\Doctrine\Common\Collections\ArrayCollection
*/
private $categories;
Relation inverse dans l'entité Category
/**
* @ORM\ManyToMany(targetEntity="App\Entity\Articles", mappedBy="categories)
*/
private $articles;
Pour faire une relation ManyToMany avec paramètres il faut créer une entité intermédiaire
// Panier
/**
* @ORM\OneToMany(targetEntity="PanierProduit", mappedBy="panier")
*/
// PanierProduit
/**
* @ORM\ManyToOne(targetEntity="Panier", inversedBy="panierProduits")
*/
/**
* @ORM\ManyToOne(targetEntity="Produit", inversedBy="panierProduits")
*/
// Produit
/**
* @ORM\ManyToOne(targetEntity="PanierProduit", mappedBy="produit")
*/
Pour afficher un formulaire on utilise une classe "FormType"
L'ajout de champ se fait dans le méthode buildForm() en utilisant la méthode "add" de l'objet FormBuilderInterface
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('title')
->add('content')
;
}
Sf définit automatiquement le type de champ en fonction des annotations ORM de l'entité
// use Symfony\Component\Form\Extension\Core\Type;
$builder->add('status', Type\ChoiceType::class, array(
'label' => 'article.status',
'choices' => array(
'status.public' => 'public',
'status.private' => 'private',
'status.draft' => 'draft',
),
'expanded' => true, // Affiche le champ sous forme de radio
'multiple' => false, // Choix multiple ou non ()
'required' => true, // champ requis ou non
) ) // type string dans la db
Pour créer un champ qui n'est pas dans l'entité, utiliser l'option 'mapped' => false
Lorsqu'un champ affiche une liste d'entités (par exemple la liste des catégories), il est possible de modifier la requête faite par Sf
->add('categories', null, array( // Type\EntityType::class
'label' => 'article.categories',
'expanded' => true,
'query_builder' => function(EntityRepository $er) {
return $er->createQueryBuilder('c')
->orderBy('c.name', 'ASC')
// ->where('c.status = "public"')
;
}
))
Il est possible de modifier le formulaire en fonction de l'entité (par exemple si l'article contient une image, afficher le champ pour la supprimer), on utilise dans ce cas les événements de formulaire dans la méthode BuildForm
// Evénement pour ajouter ou non le champ "deleteImage"
$builder->addEventListener(FormEvents::PRE_SET_DATA, function(FormEvent $event) {
$entity = $event->getData(); // Entité envoyée au formulaire
$form = $event->getForm(); // Récupére le formulaire
if (!is_null($entity->getImage())) { // S'il y a une image dans mon article
// Ajout du champ "deleteImage" seulement s'il y a une image
$form->add('deleteImage', Type\CheckboxType::class, array(
'label' => 'image.delete',
'required' => false, // Désactive le required
));
}
});
Exemple: inclure le formulaire d'ajout d'image dans le formulaire d'article
// use App\Form\ImageType;
$builder->add('image', ImageType::class)
Pour la gestion d'utilisateur, nous utilisont un bundle qui va nous proposer des formulaires de connexion, enregistrement par défaut
php bin/console fos:user:create --super-admin
Twig est un moteur de template, il propose un langage simplifié pour créer nos vues
{% if condition %} {% else %} {% endif %}
Test si une variable n'est pas vide
{% if var is not empty %}{% endif %}
Test si une variable existe
{% if var is defined %}{% endif %}
Pour les tests booléens : and|or
{% for entity in entities %}{% endfor %}
Si la boucle est vide
{% for user in users %}
<li>{{ user }}</li>
{% else %}
<p>Aucun utilisateur</p>
{% endfor %}
Dans une boucle, la variable "loop" permet d'obtenir des informations sur la boucle:
{{ loop.index }} {# index de la boucle #}
{{ loop.revindex }} {# total - index #}
{{ loop.first }} {# booléen si on est dans la première itération #}
{{ loop.last }} {# booléen si on est dans la dernière itération #}
{{ loop.length }}
Boucle for simple
{% for i in 1..10 %}
{% endfor %}
{% for a in 'a'..'z' %}
{% endfor %}
Les filtres permettent de modifier une variable
{{ var|lower }}
{{ var|upper }}
{{ var|date('d/m/Y') }}
{{ var|raw }} {# Affiche au format HTML #}
{{ var|nl2br }}
{% include 'template.html.twig', {var: 'var'} %} {# Inclus un template #}
{{ dump(article) }}
{{ 'article.count'|trans }}
{{ 'article.message'|trans({'%title%': entity.title}) }}
{{ 'article.count|transchoice(count, {'%count%': count})}}
article:
message: Article avec le titre %title%
count: "{0} Aucun article|{1} 1 article|]1,Inf[ %count% articles"
Les blocks permettent de redéfinir une partie d'une vue
{# parent.html.twig #}
<div class="partie-1">
{% block partie1 %}
{% endblock %}
</div>
{# child.html.twig #}
{% extends 'parent.html.twig' %}
{% block partie1 %}
{{ parent() }} {# affiche le contenu du block "partie1" de la vue parente #}
...
{% endblock %}
{{ path('nom_de_la_route', {'param1': 1}) }}
{{ render(controller('App\\Controller\\ArticleController::recentArticles', { 'count' : 10 })) }}
Afficher un formulaire complet
{{ form(form) }}
Décomposer les champs
{{ form_start(form) }} {# Balise <form> #}
{{ form_errors(form) }} {# Erreurs globales #}
{{ form_row(form.champ) }}
{{ form_end(form) }}
Il est possible d'ajouter des fonctions et des filtres à Twig en créant une extension
// src/Twig
namespace App\Twig;
use Twig\Extension\AbstractExtension;
use Twig\TwigFilter;
use Twig\TwigFunction;
class AppExtension extends AbstractExtension
{
public function getFilters()
{
return [
new TwigFilter('nom', [$this, 'nomDeLaMethode'])
];
}
}
{% if is_granted('IS_AUTHENTICATED_REMEMBERED') %}
{{ app.user.username }}
{% endif %}
Un repository contient les requêtes vers la base de données
public function findPublishedByUser(User $user)
{
$qb = $this->createQueryBuilder('a')
->andWhere('a.pusblished = :published')
->andWhere('a.user = :user')
->setParameter('published', true)
->setParameter('user', $user) // Objet User
->getQuery()
;
// Retourne un tableau d'objets Article
return $qb->getResult(); // $qb->execute();
}
L'objet Paginator permet de gérer plus facilement la pagination
public function findByPage($page = 1, $count = 10)
{
$offset = ($page - 1) * $count; // Calcul offset de la requête
$queryBuilder = $this->createQueryBuilder('a')
->select('a, u, c') // Ajoute l'utilisateur et les catégories pour optimiser le nombre de requêtes
->leftJoin('a.user', 'u')
->leftJoin('a.categories', 'c')
->setFirstResult($offset) // OFFSET
->setMaxResults($count) // LIMIT
->orderBy('a.id', 'DESC')
;
return new Paginator($queryBuilder); // Liste + nombre total
}
Retourner un seul objet
public function findLastByUser($user)
{
$qb = $this->createQueryBuilder('a')
->andWhere('a.user = :user')
->setParameter('user', $user) // Objet User
->orderBy('a.id', 'DESC')
->setMaxResults(1)
->getQuery()
;
return $qb->getOneOrNullResult(); // 1 objet Article ou null
}
Controller permet de traiter la requête client et retourne une réponse
// Sf < 4.2
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted;
use Symfony\Component\Translation\TranslatorInterface;
/**
* Définit un prefix pour le nom et le chemin des routes de ce controller
* @Route('admin/article', name="article_admin_")
*
* Autorise seulement les membre avec ces rôles
* @Security("is_granted('ROLE_ADMIN') and is_granted('ROLE_MODERATOR')")
*/
class ArticleController extends AbstractController
{
/**
* @Route("/index", name="index")
*/
public function index(Request $request, ArticleRepository $articleRepository /* SF 4 */) : Response // Sf < 4 => indexAction
{
// SF < 4
/*
$em = $this->getDoctrine()->getManager();
$articles = $em->getRepository(Article::class)->findAll();
*/
// SF 4
$articles = $articleRepository->findAll();
// Retourne une réponse avec le rendu de la vue twig
return $this->render('admin/article/index.html.twig', array(
'articles' => $articles,
));
}
/**
* @Route("/new", name="new")
*/
public function new(Request $request, TranslatorInterface $translator /* SF 4 */) : Response
{
$entity = new Article;
$entity->setUser($this->getUser()); // Récupére l'utilisateur connecté
// Création du formulaire à partir de la classe ArticleType
$form = $this->createForm(ArticleType::class, $entity);
// Envois la requête au formulaire
$form->handleRequest($request);
// Tester si le formulaire est envoyé et s'il est valide
if ($form->isSubmitted() && $form->isValid()) {
$em = $this->getDoctrine()->getManager();
$em->persist($entity); // PDO -> prepare
$em->flush(); // PDO -> execute
// SF < 4.2
//$translator = $this->get('translator');
$this->addFlash('success', $translator->trans('article.new.success'));
return $this->redirectToRoute('article_admin_index');
}
return $this->render('admin/article/new.html.twig', array(
'form' => $form->createView()
));
}
}
Test si la requête est en AJAX
if ($request->isXmlHttpRequest()) {
return $this->json(array(
'var' => $var,
));
}
Pour trouver la classe utilisée dans un service:
php bin/console debug:container security.token_storage
La gestion des menus se fait avec KNPMenuBundle
Pour ajouter un nouveau menu, créer la class MenuBuilder
// src/Form/AdminMenuBuilder.php
public function createMenu()
{
$menu = $this->factory->createItem('root');
// Ajouter un item
$parent = $menu->addChild('chaine.a.traduire', [
'route' => 'nom_de_la_route',
'routeParameters' => ['id' => 1 ],
'attributes' => ['class' => 'test']
]);
// ajouter un sous-menu, on ajoute des item au menu parent
$parent->addChild('sous-item');
// Item avec lien externe
$menu->addChild('page.facebook', ['uri' => 'http://www.facebook.com']);
}
Aide à ecrire les classes
Créer une entité
php bin/console make:entity
make:form
make:controller
Pour générer un CRUD admin propre:
- Faire une copie de l'entité dans le dossier Entity\Admin
- Faire la commande
php bin/console make:crud
et donner le lien vers l'entité du dossier adminAdmin\User
- Une fois le crud généré, supprimer le fichier dans Entity\Admin
- Faire un trouver/remplacer de "App\Entity\Admin\User" par "App\Entity\User"
- Faire un trouver/replacer dans les vues de "admin_user/" par "admin/user/"
- Atlantic18/DoctrineExtensions
- Case mismatch between loaded and declared class names: "App\Controller\ArticleController" vs "App\controller\ArticleController".
Problème d'espace de nom ou de nom de classe
-
The file "C:\xampp\htdocs\PHPOO\ProjetSymfony\config/services.yaml" does not contain valid YAML in Problème de validation YAML
-
The file was found but the class was not in it, the class name or namespace probably has a typo. Soit le nom de la classe ne correspond pas au nom du fichier, soit l'espace de nom n'est pas bon
-
Object of class App\Entity\Category could not be converted to string Il manque la méthode __toString dans l'objet
-
Unable to generate a URL for the named route "admin_article_add" as such route does not exist. Le nom de la route (dans un {{ path() }} par exemple) n'existe pas, faire la commande debug:router pour afficher les routes
-
Unable to find template "admin/article/list.html.twig" Soit le nom dans la méthode "render" du controller n'est pas bon, soit le fichier n'a pas été créé
-Controller "App\Controller\Admin\ArticleController::edit()" requires that you provide a value for the "$entity" argument. Either the argument is nullable and no null value has been provided, no default value has been provided or because there is a non optional argument after this one.
Oublis du typage d'un argument de méthode dans le controller (Article $entity)