Sous Windows, pour savoir quel processus utilise un port, lancez la commande Get-Process
dans Powershell :
Exemple pour le port 5433
:
Get-Process -Id (Get-NetTCPConnection -LocalPort 5433).OwningProcess
Publié le :
Sous Windows, pour savoir quel processus utilise un port, lancez la commande Get-Process
dans Powershell :
Exemple pour le port 5433
:
Get-Process -Id (Get-NetTCPConnection -LocalPort 5433).OwningProcess
Publié le :
Voici un modèle de composant React minimal :
// MyComponent.tsx
import React, { ReactNode } from 'react';
import styles from "./MyComponent.module.scss";
interface Props {
children: ReactNode;
notRequiredProperty?: string;
}
export const MyComponent: React.FC = ({ children, notRequiredProperty }: Props) => {
return <div className={styles.myComponentClass}>{children}< /div>;
};
Explications :
<MyComponent children={<p>EXAMPLE</p>} />
MyComponent.module.scss
, dans lequel se trouve une règle CSS pour la
classe myComponentClass
.children
.Props
pour définir les propriétés du composant. Cela facilite l’autocomplétion et
permet la validation du typage à la compilation.notRequiredProperty
est facultative lorsqu’on appelle le composant.Si on souhaite créer un wrapper pour un composant existant, on veut souvent qu’il accepte les mêmes propriétés. Dans ce cas, on peut déclarer l’interface et le composant ainsi :
type Props = SomeComponentToWrapProps & {
extraProperty1: string
};
export const MyComponent: React.FC = ({ extraProperty1, inheritedProp1, ...others }: Props) => {
// [...]
};
Explications :
SomeComponentToWrapProps
, avec celle déclarée ici.others
.Publié le :
Contrairement à Angular, React propose peu de solutions par défaut aux problèmes les plus courants. Il faut souvent ajouter des modules en dépendance, et il est parfois difficile de ne pas se perdre dans la jungle des modules pour React.
Voici une liste de modules et de pratiques utiles pour les besoins courants.
On peut initialiser facilement un projet React avec l’outil CRA
(Create React App).
Il crée l’arborescence de base, via une simple commande comme npx create-react-app my-app --template typescript
.
Le module react-router fait ça très bien.
Pour récupérer des données auprès d’une API ou via d’autres méthodes asynchrones, on peut utiliser SWR ou react-query.
Si on souhaite gérer un état globale de l’application au niveau local, on peut utiliser redux avec redux-toolkit ou alors recoil.
Le module react-hook-form fait ça très bien. Il supporte notamment Yup pour gérer la validation de champs.
Le module react-i18next permet de gérer une appli multilingue.
Pour restreindre des classes CSS à un composant en particulier, on peut utiliser le concept de modules CSS.
Les principales bibliothèques graphiques sont disponibles pour React, et notamment Material UI. Elle fonctionne très bien, sauf avec le hook-form. C’est problématique et cela nécessite de recréer un composant « Contrôlable », pour chaque type de champ de formulaire.
Plusieurs bibliothèques pour les tests fonctionnent bien avec React, notamment jest.
On peut combiner prettier et eslint, pour normaliser la base de code. Il existe des extensions à eslint spécialement pour React, afin de détecter et remonter si certaines mauvaises pratiques sont utilisées.
Pour documenter et présenter une bibliothèque de composants, on peut utiliser Storybook. Cela permet de manipuler et tester chaque composant, dans une interface dédiée.
Pour utiliser plus facilement la base de données locale IndexedDB, on peut y accéder via dexie.
Pour extraire les données d’un jeton JWT, on peut utiliser jwt-decode.
Publié le :
Régulièrement en Javascript, on veut écouter les changements sur un élément, mais ne les prendre en compte qu’à
intervalle régulier.
Par exemple, si on écoute le déplacement de la souris pour lancer un traitement, on n’a pas besoin de l’exécuter
à chaque changement de position, mais uniquement tous les 300 ou 500ms.
Pour éviter l’effet « rebond », on utilise alors une fonction de debounce, ou dans React, un hook.
import { useEffect, useState } from 'react';
/**
* Delays the value change, so it’s only committed once during the specified duration.
*
* Usage :
* <pre>
* const debouncedValue = useDebouncedValue(realTimeValue, 500);
* <pre>
*/
export const useDebouncedValue = <T>(input: T, duration = 500): T => {
const [debouncedValue, setDebouncedValue] = useState(input);
useEffect(() => {
const timeout = setTimeout(() => {
setDebouncedValue(input);
}, duration);
return () => {
clearTimeout(timeout);
};
}, [input, duration]);
return debouncedValue;
};
On veut afficher un message dans la console au changement de valeur du champ de formulaire,
mais limiter ce traitement avec un debounce.
import React, { useEffect, useState } from 'react';
import { useDebouncedValue } from './utils/debounce';
export default (() => {
const [myValue, setMyValue] = useState('');
const myDebouncedValue = useDebouncedValue(myValue, 500);
useEffect(() => {
console.log('This log is displayed when the value of debouncedFilterNom changes', myDebouncedValue);
}, [myDebouncedValue]);
const handleValueChange = async (event: any) => {
setMyValue(event.target.value);
};
return (
<form>
<input type="text" onChange={handleValueChange} value={myValue} />
</form>
);
}) as React.FC;
Publié le :
Voici une fonction pour construire une queryString à partir d’un objet.
Version JavaScript :
export const buildQueryString = (parameters) => {
const paramParts = Object.entries(parameters)
.map(([key, value]) => `${key}=${encodeURI(value)}`);
return paramParts.length > 0 ? `?${paramParts.join('&')}` : '';
};
Version TypeScript :
export const buildQueryString = (parameters: { [key: string]: string }) => {
const paramParts = Object.entries(parameters)
.map(([key, value]) => `${key}=${encodeURI(value)}`);
return paramParts.length > 0 ? `?${paramParts.join('&')}` : '';
};
Explications :
&
et ?
est ajouté en préfixe de la chaîne.Publié le :
Il est possible de modifier l’auteur de tous les anciens commits d’un dépôt Git.
Pour cela, utilisez la commande suivante, après l’avoir personnalisée selon vos besoins :
git filter-branch --env-filter '
WRONG_EMAIL="wrong@email.com"
NEW_NAME="John Doe"
NEW_EMAIL="j.doe@email.com"
if [ "$GIT_COMMITTER_EMAIL" = "$WRONG_EMAIL" ]
then
export GIT_COMMITTER_NAME="$NEW_NAME"
export GIT_COMMITTER_EMAIL="$NEW_EMAIL"
fi
if [ "$GIT_AUTHOR_EMAIL" = "$WRONG_EMAIL" ]
then
export GIT_AUTHOR_NAME="$NEW_NAME"
export GIT_AUTHOR_EMAIL="$NEW_EMAIL"
fi
' --tag-name-filter cat -- --branches --tags
Explications :
On indique l’adresse email utilisée par tous les commits que l’on souhaite modifier, ainsi que le nom d’utilisateur et l’adresse email correcte que l’on souhaite utiliser à la place.
Publié le :
Sous Linux, l’utilitaire ncdu (pour NCurses Disk Usage) permet de trouver quel fichier ou répertoire vous bouffe tout votre espace disque.
Il est disponible sur les dépôts officiels Debian et Ubuntu et donc facile à installer.
sudo apt install ncdu
ncdu
Note : ncdu a été réécrit dans une v2 encore récente. Selon la version des dépôts, c’est peut-être la v1 qui sera installée.
Publié le :
Dans une application ou un site web, on peut stocker des informations dans le navigateur de l'utilisateur, pour un nom de domaine spécifique.
Deux approches principales : les cookies et le Storage
. Pour la seconde, deux possibilités :
sessionStorage
ou localStorage
.
La principale différence, c'est que les cookies sont accessibles côté client et côté serveur.
À l'inverse, il n'y a aucun moyen côté serveur d'accéder au contenu ni du localStorage
ni du sessionStorage
.
Remarque :
Quand on parle de "sessions" pour le sessionStorage
, il n'y a aucun rapport avec les sessions
entre client/serveur comme on peut les manipuler en PHP, par exemple.
Les données stockées en sessionStorage sont rattachées à un onglet.
Si on recharge la page au sein d'un même onglet, les données sont conservées.
En revanche, elles expirent à sa fermeture.
Les données stockées en sessionStorage sont rattachées à un onglet.
Remarque :
Selon le navigateur, si on restaure un onglet fermé précédemment, on récupère le sessionStorage
tel qu'il était avant fermeture. C'est le comportement pour les navigateurs basés sur Chrome.
De même, si on duplique un onglet ouvert, la copie hérite d'une copie du sessionStorage
de l'onglet original.
Publié le :
Pour faire du refactoring automatique en PHP, on peut utiliser son IDE ou la lib Rector (installable via Composer).
Elle permet notamment d'automatiser la migration de code PHP vers une version plus récente de PHP.
Quelques exemples :
??
et ?:
Il est également possible de créer ses propres règles de refactoring.
Grafikart a sorti une vidéo de présentation de l'outil.
Publié le :
Si vous construisez des APK Android avec Cordova, vous pouvez rencontrer ce genre d'erreur :
[...]
> Task :app:preReleaseBuild FAILED
FAILURE: Build failed with an exception.
* What went wrong:
Could not resolve all files for configuration ':app:releaseRuntimeClasspath'.
> Could not resolve com.android.support:support-v4:27.+.
Required by:
project :app
> Failed to list versions for com.android.support:support-v4.
> Unable to load Maven meta-data from https://jcenter.bintray.com/com/android/support/support-v4/maven-metadata.xml.
> Could not get resource 'https://jcenter.bintray.com/com/android/support/support-v4/maven-metadata.xml'.
> Could not GET 'https://jcenter.bintray.com/com/android/support/support-v4/maven-metadata.xml'.
> Read timed out
[...]
Cela est dû à la coupure du dépôt jcenter.
La solution pour le remplacer est décrite dans plein de tickets sur StakOverflow (ex: Question #67418153).
En bref : il faut modifier le fichier build.gradle
.
Malheureusement, vous n'avez peut-être pas accès à ce fichier, si c'est une Plateforme d'Intégration Continue qui se charge du build.
Dans ce cas, ce fichier n'est probablement pas versionné, mais générée automatiquement dans un répertoire nommé platforms/
.
Entre chaque build, veillez à bien supprimer ce répertoire platforms/
.
Ainsi, le fichier build.gradle
sera régénéré en utilisant des dépôts corrects.
Publié le :
bin/console
bin/console debug:autowiring
Cela liste les interfaces disponibles.
Pour en rechercher une concernant un sujet (ex: cache, mail, markdown)
bin/console debug:autowiring markdown
Pour voir toutes les implémentations de ces interfaces
bin/console debug:container
Ou pour avoir le détail sur l'une d'entre elles (ex: markdown)
bin/console debug:container markdown
Pourvoir les paramètres présents dans le conteneur
bin/console debug:container markdown
bin/console debug:container --parameters
Cela liste toutes la configuration par défaut pour le bundle.
Pour voir la config actuelle
bin/console debug:config MyBundle
bin/console debug:router --show-controllers
Affiche la liste de toutes les routes et les contrôleurs/actions associés :
La commande make
permet de générer un squelette de classe
(Command, TwigExtension, ...) :
bin/console make
dump($someVariable);
Pour afficher une variable et stopper l'exécution :
// "Dump and Die"
dd($someVariable);
Pour afficher toute la syntaxe ainsi que les variables globales disponibles dans Twig :
bin/console debug:twig
La section _default
est héritée par toutes les déclarations suivantes.
C'est pourquoi si on y utilise bind
pour binder des arguments, ils seront
disponibles pour tous les services.
On peut typer ces arguments. Ex:
services:
_default:
bind:
bool $isDebug: '%kernel.debug%'
Psr\Log\LoggerInterface $someLogger: '@monolog.logger.some_logger'
Quand il y a plusieurs implémentations d'une interface
(par exemple LoggerInterface, cf.bin/console debug:autowiring log
), on peut
accéder à celui que l'on veut en nommant l'argument comme indiqué.
Ex:
public function __constructor(LoggerInterface $consoleLogger) {}
// à la la place de
public function __constructor(LoggerInterface $logger) {}
Un alias de service peut être créé en une ligne :
services:
Some\Path\To\SomeService $newName: '@id_of_the_target_service'
Monolog peut loguer des messages sur des channel spécifiques. Pour cela il faut :
# ex: dans monolog.yaml
monolog:
channels: ['my_new_channel']
bin/console debug:autowiring log
)public function __constructor(LoggerInterface $myNewChannelLogger) {}
Ajoute des extensions doctrine (Slug, Blameable, Softdeleteable, ...)
Ajoute le filter twig ago
, pour afficher depuis quand la date est passée.
Ajoute une commande Symfony (make:factory
) pour générer des Factory pour les entités.
Compatible avec le bundle ci-dessous
Permet de générer du faux contenu de manière intelligente.
Publié le :
Cet article est une synthèse de la documentation officielle : Forms.
Dans Symfony, un formulaire et un champ sont représentés tous deux par la FormInterface
.
Un formulaire contient des champs ou des sous-formulaires enfants, pouvant eux-mêmes en avoir.
(Il s'agit là du Design Pattern Composite.)
La FormTypeInterface
permet de représenter un type de champ/formulaire (ex: ContactFormType
,
BirthdayType
, ...). En fonction de ce type, le champ/formulaire aura telles ou telles options, et tel ou tel rendu.
La classe BaseType
l'implémente. En héritent deux sous-classes : FormType
pour les champs et ButtonType
pour
les boutons.
Pour définir un nouveau type de champ/formulaire, il faut donc implémenter FormTypeInterface
.
Pour simplifier cette tâche, on étend AbstractType
, qui implémente déjà les méthodes avec des comportements neutres.
De plus, on surcharge éventuellement sa méthode getParent()
, pour indiquer le type précis dont on veut hériter les
propriétés (ex: EmailType
, EntityType
, ...).
Cette mécanique consistant à ne pas utiliser l'héritage direct PHP des FormTypes qui nous intéresse, évite de
devoir appeler nous-même les méthodes parentes avant d'y ajouter nos traitements spécifiques.
L'appel au parent est fait automatiquement et nous n'avons qu'à nous occuper des traitements spécifiques.
Exemples natifs :
use \Symfony\Component\Form\AbstractType;
// Héritage PHP du type neutre
class DateType extends AbstractType {
public function getParent() {
// Héritage des propriétés/options/... du type de base
return FormType::class;
}
}
// Héritage PHP du type neutre
class BirthDayType extends AbstractType {
public function getParent() {
// Héritage des propriétés/options/... de DateType
return DateType::class;
}
}
// Héritage PHP du type neutre
class FormType extends AbstractType {
public function getParent() {
// Pas d'héritage de propriétés/options/...
return null;
}
}
Pour des modifications légères de types déjà existants (par exemple ajouter des options), on peut implementer
l'interface FormTypeExtensionInterface
.
Pour créer un formulaire, on utilise un FormBuilder
ou on laisse Symfony deviner les champs en fonction d'une classe.
Pour cela :
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
// Depuis un contrôleur étendant AbstractController
$form = $this->createFormBuilder($task)
->add('task', TextType::class)
->add('dueDate', DateType::class)
->add('save', SubmitType::class, ['label' => 'Create Task'])
->getForm();
// ou en laissant Symfony deviner
$form = $this->createForm(Task::class, $task, $options);
// Depuis n'importe où
/** @var \Symfony\Component\Form\FormFactoryInterface $form */
$form = $this->formFactory
-> createBuilder(Task::class, $task, $options)
->add('task', TextType::class)
->add('dueDate', DateType::class)
->add('save', SubmitType::class, ['label' => 'Create Task'])
->getForm();
// ou en laissant Symfony deviner
$form = $this->formFactory->create(Task::class, $task, $options);
Dans le second cas, Symfony se base sur des Guesser qui devinent les champs à partir :
FormInterface
définit la méthode handleRequest()
, chargée de vérifier que tout est valide puis éventuellement
d'appeler la méthode submit()
du formulaire :
À la soumission du formulaire (ak. méthode submit()
) :
addError()
est appelée sur le champLa commande suivante permet de lister toutes les options d'un type de champ/formulaire et d'indiquer si elles sont héritées ou surchargées :
# Remplacer EntityType par n'importe quel FormType
bin/console debug:form EntityType
Il est considéré comme une bonne pratique, de rendre directement un formulaire ou un de ses champs via les fonctions de rendu dédiées. Toute modification du balisage qu'elles génèrent devrait être réalisée via la surcharge de thème.
Par défaut, plusieurs thèmes sont disponibles, notamment Bootstrap 3 et 4. Pour en ajouter :
# config/packages/twig.yaml
twig:
form_themes:
- 'form/my_custom_form_theme.html.twig'
Un formulaire ou un champ dans un template est un objet de type FormView
.
Il est possible de choisir un thème spécifique pour rendre un formulaire ou un champ, via :
{% form_theme my_form 'form/app_custom_form_theme.html.twig' %}
{% form_theme my_form.my_field 'form/app_custom_form_theme.html.twig' %}
Chaque thème définit un certain nombre de blocs, nommés d'une manière à définir quel type de champ il est susceptible
de rendre (ex: integer_widget
).
Pour le surcharger, il suffit de redéfinir le bloc dans le template courant, comme n'importe quel bloc.
Les suffixes disponibles pour les noms de bloc sont les suivants : _widget
, _row
, _label
, _help
.
Les préfixes disponibles sont listés dans la variable block_prefixes
présente dans le contexte du bloc.
Exemple : ['form','text','textarea', '_post_content']
, pour un champ nommé content
, de type TextAreaType
(héritant de TextType
héritant lui-même de FormType
), au sein du type de formulaire spécifique PostType
.
Un data transformer est chargé de transformer une donnée (sous forme de tableau, objet, entier, ...) vers une chaîne (ou un tableau de chaînes) exploitable dans un formulaire, et vice versa.
Il est représenté par l'interface DataTransformerInterface
et requiert deux méthodes :
transform()
: Donnée => (tableau de) chaîne(s) de caractèresreverseTransform()
: (Tableau de) chaîne(s) de caractères => donnéePour en ajouter un à formulaire, on utilise le FormBuilder
:
$formBuilder->addViewTransformer(new MyCustomTransformer());
// Note: il existe également cette méthode si la transformation doit être effectuée en amont de la transformation
// de la précédente
$formBuilder->addModelTransformer(new MyCustomModelTransformer());
Tous les évènements liés aux formulaires sont des FormEvent
, permettant d'accéder aux données du formulaire et
éventuellement de les modifier.
Ils ont lieu dans cet ordre chronologique :
FormEvents::PRE_SET_DATA
: on peut y modifier la donnée en fonction du formulaire
Hydratation du formulaire et ses enfants, avec la donnée
FormEvents::POST_SET_DATA
: on peut y modifier le formulaire en fonction de la donnée (ex: champ affiché en fonction
d'un autre champ)
FormEvents::PRE_SUBMIT
: on peut y changer la donnée soumise dans la requête (ex: la normaliser, la trimmer, ...)
Transformation vers un "objet" de données, via les méthodes reverseTransform()
des DataTransformer
.
FormEvents::SUBMIT
: les données sont prêtes, mais peut-être pas celles des parents
FormEvents::POST_SUBMIT
: les données sont prêtes, le composant Validation écoute cet évènement pour les valider
90% du temps, on n'a besoin d'agir que sur les évènements FormEvents::POST_SET_DATA
ou FormEvents::PRE_SUBMIT
uniquement.
Publié le :
Cet article est une synthèse de la documentation officielle : Create your First Page in Symfony.
Créer une page, c'est indiquer quelle réponse générer lorsqu'on reçoit une requête auprès d'une certaine URL.
Plus, précisément, la partie de l'URL qui nous intéresse est appellée la route, ou en français, le chemin.
Par exemple pour les URL http://mon-site.com/recherche
et http://mon-site.com/article/1234-mon-article
,
les routes seront respectivement /recherche
et /article/1234-mon-article
.
Le générateur de réponse est une fonction PHP appelée contrôleur. Dans Symfony, elle doit retourner un
objet Response
, contenant du texte (HTML, JSON, ...), une fichier binaire (ex: fichier, image, ...), ...
On appelle route le chemin présent dans l'URL permettant de déterminer qu'elle génération on souhaite.
Par défaut, les routes se déclarent dans le fichier config/routes.yaml
. Par exemple :
# the "app_lucky_number" route name is not important yet
my_route_name:
path: /some-path-to/my_page
controller: App\Controller\MyPageController::myFunctionToGenerateThePage
On a ici définit que les requêtes sur la route /some-path-to/my_page
devait recevoir une réponse
générée par la méthode myFunctionToGenerateThePage()
de la classe App\Controller\MyPageController
.
Généralement, les contrôleurs seront des méthodes de classes placées dans le répertoire src/Controller
.
Pour faciliter le mapping route-controller, vous pouvez déclarer vos routes en annotations à la place d'utiliser le fichier
routes.yaml
. Cela regroupe ainsi la route et le contrôleur au même endroit.
Pour cela, la dépendance annotations
doit être installée :
coomposer require annotations
Vous pouvez dès lors indiquer la route en annotation de votre contrôleur :
<?php
// src/Controller/MyPageController.php
namespace App\Controller;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
class MyPageController
{
/**
* @Route("/some-path-to/my_page", name="my_route_name")
*/
public function myFunctionToGenerateThePage(): Response
{
return new Response(
'<html><body>Hey guys!</body></html>'
);
}
}
Remarque : Si vous utilisez PHP 8, vous pouvez utiliser directement les annotations du langage :
#[Route('/some-path-to/my_page', name: 'my_route_name')]
Pour lister les routes disponible et les contrôleurs associés, utilisez la commande suivante :
bin/console debug:router
Pour faciliter l'écriture de pages HTML, Symfony propose le moteur de templates Twig.
Il faut installer la dépendance :
coomposer require twig
Vous pouvez désormais utiliser ce moteur dans vos contrôleurs. Pour y avoir accès, la solution
la plus simple est de faire étendre vos contrôleurs de la classe AbstractController
. Ex :
// src/Controller/MyPageController.php
// ...
+ use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
- class MyPageController
+ class MyPageController extends AbstractController
{
// ...
}
La méthode render()
vous permet de transformer un template twig en du code HTML et de
le placer dans un objet Response
:
<?php
// src/Controller/MyPageController.php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
class MyPageController extends AbstractController
{
/**
* @Route("/some-path-to/my_page", name="my_route_name")
*/
public function myFunctionToGenerateThePage(): Response
{
return $this->render('some-path-to/my-page.html.twig', [
'who' => 'guys',
]);
}
}
{# templates/some-path-to/my-page.html.twig #}
<html>
<body>
<h1>Hey {{ who }}!</h1>
</body>
</html>
Remarques :
Ici, on a définit la variable who
avec pour valeur guys
et on l'a transmise au template
en argument de la fonction render()
. La syntaxe {{ my_var }}
permet de l'afficher.
En général, on n'ajoute pas les balises <html>
et <body>
directement dans le template.
À la place, on utilise l'héritage de Twig et notre template étend alors base.html.twig
, par exemple.
Voici les principaux répertoires de votre projet Symfony :
├── bin/
│ └── console # exécutable pour lancer les commandes Symfony
├── config/ # répertoire contenant les fichiers de config des routes, services et dépendances
├── public/ # répertoire contenant tous les fichiers accessibles publiquement
│ ├── a-public-file.txt
│ └── index.php
├── src/ # répertoire contenant tout votre code PHP
├── templates/
├── var/ # répertoire contenant les fichiers auto-générés
│ ├── cache/
│ └── log/
├── vendor/ # répertoire contenant les dépendances installées avec Composer (dont Symfony !)
Publié le :
Cet article est une synthèse de la documentation officielle : Routing.
Qu'est-ce qu'une route ?
C'est une configuration qui décrit comment trouver le contrôleur à exécuter pour un chemin (ou un ensemble de chemins) donné.
Pour cela, elle va décrire précisément :
Elle peut être de différents types, Symfony gérant nativement annotation, yaml, xml et php.
Est-il possible d'avoir une route par langue pour un même contrôleur ? (ex: /about-us
et /a-propos-de-nous
)
Oui, en indiquant un tableau associatif pour path :
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
/**
* @Route({
* "en": "/about-us",
* "nl": "/over-ons"
* }, name="about_us")
*/
public function about(): Response
{
// ...
}
Pour indiquer une regex décrivant le format attendu pour l'un des paramètres de la route, deux syntaxes sont possibles :
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
/**
* @Route("/blog/{page}", name="blog_list", requirements={"page"="\d+"})
*/
public function list(int $page): Response
{
// ...
}
/**
* @Route("/blog/{slug<[a-z-]+>}", name="blog_show")
*/
public function show(string $slug): Response
{
// ...
}
Explications :
requirements
pour définir la regex du paramètre page
.Même chose, deux possibilités :
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
/**
* @Route("/blog/{page}", name="blog_list", requirements={"page"="\d+"}, defaults={"page"="homepage"})
*/
public function list(int $page): Response
{
// ...
}
/**
* @Route("/blog/{slug<[a-z-]+>?homepage}", name="blog_show")
*/
public function show(string $slug): Response
{
// ...
}
Notes :
"/blog/{slug?homepage}"
).?
, alors le paramètre devient optionnelLa génération est fournie via la méthode generate()
du service
UrlGeneratorInterface
.
Elle peut générer plusieurs choses selon le paramètre $referenceType
qu'on lui fournit :
UrlGeneratorInterface::ABSOLUTE_URL
: une URL absolue (ex: http://example.com/dir/file
)UrlGeneratorInterface::ABSOLUTE_PATH
: un chemin absolu (ex: /dir/file
)UrlGeneratorInterface::RELATIVE_PATH
: un chemin relatif au chemin de la requête courante (ex: ../parent-file
)UrlGeneratorInterface::NETWORK_PATH
: un chemin réseau (ex: //example.com/dir/file
)Le RedirectController
fourni par Symfony, permet des redirections temporaires ou permanente
Il s'utilise directement par configuration dans le routing.yaml
.
L'ArgumentResolver
rend disponible automatiquement les attributs de la requête par injection dans
l'action de contrôleur.
La variable injectée doit avoir le nom de l'attribut (commençant par un _
). Ex: $_route_params
, $_controller
, ...
Note : On peut également les récupérer classiquement via la Request dans le bag attribute
.
Il est possible de définir des conditions spécifiques de match pour une route.
Celles-ci sont définies par de l'expression language.
Ex :
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
class DefaultController extends AbstractController
{
/**
* @Route(
* "/contact",
* name="contact",
* condition="context.getMethod() in ['GET', 'HEAD'] and request.headers.get('User-Agent') matches '/firefox/i'"
* )
*
* expressions can also include config parameters:
* condition: "request.headers.get('User-Agent') matches '%app.allowed_browsers%'"
*/
public function contact(): Response
{
// ...
}
}
Symfony fournit 2 commandes utiles pour travailler avec le routage :
bin/console debug:router --show-controllers
: pour lister toutes les routes gérées et par quels contrôleursbin/console debug:router name_of_the_route
: pour afficher la configuration complète d'une routebin/console router:match /some/url [--method=GET]
: pour afficher si l'URL match avec une route et si oui laquellePour pouvoir charger des routes provenant d'une source spécifique (ex: base de donnée, fichier .ini, ...), on peut créer un Route Loader spécifique.
Celui-ci doit étendre Symfony\Component\Config\Loader\Loader\Loader
.
En surchargeant la méthode supports()
, il peut gérer des types de route spécifiques.
Publié le :
Un composant est une bibliothèque écrite en PHP, pouvant fonctionner sans Symfony.
Exemples : Mailer, HttpClient, Form, ...
Un bundle est un plugin pour Symfony, servant d'interface entre une bibliothèque et Symfony.
Il permet par exemple de configurer facilement la bibliothèque via Symfony.
Exemples : twig-bundle
qui permet de configurer Twig
Un bridge est un élément Symfony permettant d'étendre une bibliothèque en y ajoutant
des choses utiles à Symfony
Exemples : twig-bridge
qui ajoute des fonctions à twig
La liste des best practices est disponible ici.
├── assets/
├── bin/
│ └── console
├── config/
├── public/
│ └── index.php
├── src/
│ └── Kernel.php
├── templates/
├── tests/
├── translations/
├── var/
│ ├── cache/
│ └── log/
├── vendor/
Non recommandé :
config/
: peut poser problème pour le déploiement des recipes FlexConfigurable :
/var/cache/
et /var/log/
: via surcharge des méthodes getCacheDir()
et getCacheLog()
du Kernel.php
templates/
: via twig.default_path
(par défaut dans le twig.yaml
)translations/
: via framework.translator.default_path
(par défaut dans le translation.yaml
)Risqué à modifier :
public/
: vérifier que tous les chemins restent corrects dans l'index.php
.
Il faut également ajouter la propriété extra.public-dir
dans le composer.json
(si on utilise Flex)
vendor/
: vérifier que tous les chemins restent corrects dans l'index.php
.
Il faut également ajouter la propriété config.vendor-dir
dans le composer.json
.
C'est un plugin Composer qui va permettre d'ajouter/modifier les commandes Composer.
Par exemple, la commande require
va permettre d'installer des paquets particuliers appelés Recipes.
Une recipe est un paquet ne contenant qu'une liste de dépendances (via un composer.json
), ainsi
qu'une liste de modifications à effectuer (via un manifest.json
) :
Certains recipes sont officielles (= maintenues par la code team Symfony), d'autres contrib.
Elles sont toutes visibles sur https://flex.symfony.com.
Les fichiers manifest.json
des recipes sont lues via les Configurators.
Ex de fichier :
{
"bundles": {
"Alexandre\\EvcBundle\\AlexandreEvcBundle": ["all"]
},
"copy-from-recipe": {
"config/": "%CONFIG_DIR%/"
},
"env": {
"EVC_API": "Enter the api version provided by evc.de support",
"EVC_USERNAME": "Enter your reseller account number",
"EVC_PASSWORD": "Enter your api password, not the password to connect on evc.de website"
}
}
Cette configuration nécessite par exemple 3 Configurators pour :
Les configurators natifs sont visibles ici.
De nouvelles commandes Composer ont été créées pour interagir avec Flex :
composer recipes
: liste les recipes disponibles (et affiche les mises à jour éventuelles)composer sync-recipes [--force]
: met à jour une recipeLorsqu'une erreur (PHP) est levée :
ErrorHandler
la transforme en exception, qui est levéeLorsqu'une exception est levée :
HttpKernel
l'attrape et dispatche un évènement kernel.exception
ErrorListener
effectue un forward vers l'ErrorController
ErrorController
retourne une Response contenant l'erreur formatée via le ErrorRenderer
La classe Event
contient deux méthodes :
stopPropagation()
: elle indique qu'on ne souhaite pas que les prochains listeners ne traitent l'évènementisPropagationStopped()
: elle indique si on a demandé à stopper la propagationÀ l'instar des services, on utilisait historiquement une chaîne de caractères comme identifiant de l'évènement
(ex: kernel.terminate
).
Depuis Symfony 5, on utilise directement le nom de sa classe (ex: Symfony\Component\Mailer\Event\MessageEvent
).
Pour lister les évènements disponibles et leurs listeners, utiliser la commande :
bin/console debug:event-dispatcher
Best practice :
Le subscriber est plus pratique d'utilisation, car il ne nécessite aucune configuration (en fichier yaml).
Il suffira qu'il implémente EventSubscriberInterface
.
Les évènements du Kernel sont visibles ici.
Il y a deux versions principales à connaitre :
La dernière version stable (ex: 5.1.8
) : elle inclue toutes les nouvelles fonctionnalités, corrections
de bogue et patchs de sécurité.
La version LTS (Long Term Support) (ex: 4.4.16
) : elle inclue toutes les dernières fonctionnalités
de la version majeure, ainsi que les nouvelles corrections (pendant 3 ans au total) de bogue et patchs de sécurité
(pendant 4 ans au total).
La version LTS est changée tous les 2 ans. Comme les versions mineures sortent tous les 6 mois, on a seulement 4 versions mineures par majeures.
Les versions non-LTS sont supportées 8 mois.
Note : en version 2.x, il y a eu plusieurs versions LTS
D'une version mineure à l'autre, Symfony garantie un fonctionnement à l'identique pour toute
l'API publique (= ni @internal
, ni @experimental
).
Les dépréciations indiquent qu'une fonction/classe/... ne devrait plus être utilisée grâce à l'annotation @deprecated
.
Comment surcharger un champ de formulaire (FormType) ?
On crée une extension de type.
Comment surcharger une route définie par un bundle ?
Il faut :
Comment surcharger un contrôleur ?
On peut :
Comment modifier un service ?
On peut :
service.yaml
) et le faire pointer vers un nouveau serviceLe framework est compatible avec de nombreuse PSR :
PSR-1/PSR-2 : coding standards
PSR-4 : namespaces et autoloading
PSR-3 : logger (via MonologBundle)
PSR-6/PSR-16 : cache
PSR-11 : conteneur de services
PSR-14 : dispatcheur d'évènements
HttpClient est compatible : PSR-7/PSR-17/PSR-18
HttpPlug est compatible avec l'implémentation : PSR-7
Publié le :
Cet article est une synthèse de la documentation officielle : Controller.
Qu'est-ce qu'un contrôleur ?
Son rôle est de transformer la Request
de l'utilisateur en une Response
à lui renvoyer.
Concrètement, c'est juste un callable.
On a l'habitude d'appeler contrôleur la classe contenant une action associée à une route,
mais en réalité c'est l'action en elle-même qui est un contrôleur.
Pour éviter cette incohérence, on peut n'avoir qu'une action par fichier contrôleur, nommée __invoke()
.
Comment Symfony sait-il ce qui est un contrôleur ?
C'est le fichier services.yaml
qui l'indique :
services:
# controllers are imported separately to make sure services can be injected
# as action arguments even if you don't extend any base controller class
App\Controller\:
resource: '../src/Controller/'
tags: ['controller.service_arguments']
Comme on le voit dans ce fichier, pas de contrainte sur le nommage de la classe ou de ses actions.
Tous les services dans src/Controller/
auront le tag controller.service_arguments
.
Qu'elles sont les particularités des contrôleurs, apportées via le tag controller.service_arguments
?
Ce tag permet à Symfony de :
public
(car ce sera un point d'entrée de l'application,
donc non appelé explicitement par nos services)ArgumentResolver
.
(Cf. classe RegisterControllerArgumentLocatorsPass
)
Un contrôleur doit-il retourner absolument une Response ?
Pas obligatoirement.ViewEvent
. Un listener pourra traiter cet évènement
pour forwarder vers un contrôleur en fallback.
Cette classe peut être héritée par une classe de contrôleur. (Cf. classe AbstractController
)
Elle fournit un certain nombre de raccourcis, pour accéder à des méthodes de certains services (cf. Service locator ci-après).
Il s'agit d'un proxy du Conteneur de services, contenant un sous-ensemble des services.
Par exemple, la propriété $container
d'AbstractController
en est un (et pas le vrai Conteneur).
Il donne accès à seulement certains services et arguments (cf. AbstractController::getSubscribedServices()
).
Quelle est différence entre forward et redirect ?
La redirection redirige l'utilisateur vers une nouvelle page et son navigateur fait une seconde requête.
Cela lui est donc visible. C'est une redirection HTTP.
Le forward exécute une seconde (sous-)requête directement, pour en retourner sa réponse.
L'utilisateur reçoit directement le résultat de cette seconde requête sans en avoir conscience.
C'est une redirection interne.
Note : Tous les listeners seront à nouveaux appelés, même sur la requête secondaire.
Par défaut, une redirection n'est pas permanente. Elle est conditionnée ponctuellement.
Par exemple, si on n'est pas connecté et qu'on essaie d'accéder à une page privée, on pourra
être redirigé vers la page de login. Ce n'est pas une redirection permanente, car une fois connecté, l'utilisateur
devra pouvoir y accéder.
Par défaut, le code HTTP est donc 302
. On peut le rendre permanent via le code301
.
Il existe également leurs pendants 308
(par défaut) et 307
.
Cela oblige de conserver la même méthode HTTP (GET, POST,...).
Pour effectuer une redirection dans un contrôleur, on peut utiliser plusieurs méthodes :
public function __invoke(): RedirectResponse
{
// Si on étend l'AbstractController
return $this->redirectToRoute('app_homepage');
// ou
return $this->redirect($this->generateUrl('app_homepage'));
// Ou comme on le ferait dans un contrôleur agnostique
return new RedirectResponse(
return $this->urlGenerator->generate('app_homepage')
);
}
Qu'est-ce qu'une HttpException
?
C'est une exception classique, dont le code correspond à un code HTTP (ex: 404, 403, ...).
Quand Symfony rencontre une erreur de ce type, il la transforme en une Response
évitant ainsi de retourner
une erreur 500 au client.
Symfony implémente beaucoup d'exceptions HTTP, visibles dans
Symfony\Component\HttpKernel\Exception
.
Documentation La Request contient des conteneurs de données appelés bags :
query
: paramètres d'URL (ex: ?foo=bar&bar=foo)request
: données envoyées en POSTcookies
files
: fichiers téléversésserver
: variables $_ENV et $_SERVERheaders
: variables $_SERVER['headers']attributes
: variables spécifiques au fonctionnement (ex: _route
)
Elle permet principalement d'accéder aux données dans ces conteneurs, mais propose également un certain
nombre de raccourcis, souvent pour accéder aux headers les plus communs.
Les valeurs de chaque bag sont accessibles de la même manière, via la même méthode get()
. Ex :
use Symfony\Component\HttpFoundation\Request;
/** @var Request $request */
$request->request->get('email');
Il existe des variantes permettant de filter/convertir la donnée directement :
Filtres (utilise la fonction filter()
de php derrière) :
getAlpha()
getAlnum()
getDigits()
Conversions :getInt()
getBoolean()
Comment envoyer un cookie au navigateur de l'utilisateur ?
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Cookie;
$response = new Response('A foo enters in a bar');
$response->headers->setCookie(
Cookie::create('last_visit', \time())
);
Comment supprimer un cookie du navigateur de l'utilisateur ?
use Symfony\Component\HttpFoundation\Response;
$response = new Response('A foo enters in a bar');
$response->headers->clearCookie('last_visit');
Note : il existe également removeCookie()
, qui permet (via un listener par exemple) de retirer un cookie
qui devait être ajouté via setCookie()
durant le traitement de la requête.
Documentation
Comment récupérer la session ?
Plusieurs manières possibles.
Elle est disponible dans le bag session
de la Request :
use Symfony\Component\HttpFoundation\Request;
/** @var Request $request */
$session = $request->getSession();
$emailInSession = $session->get('email');
Elle est disponible via le Service Locator d'AbstractController
:
$session = $this->get('session');
$emailInSession = $session->get('email');
On peut également l'injecter en tant que service (classe SessionInterface
).
Documentation
Un message flash est stocké dans la session.
Le principe est d'y stocker un message qui disparaitra dès qu'il est consommé/lu.
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
// Ajout d'un message flash
/** @var SessionInterface $session */
$session->getFlashBag()->add('success', 'You reach level 2!');
// ou via le raccourci d'AbstractController
/** @var AbstractController $this */
$this->addFlash('success', 'You reach level 2!');
// Consommation d'un message flash
/** @var SessionInterface $session */
$session->getFlashBag()->get('You reach level 2!');
Note :
Il existe également les fonctions peek()
et peekAll()
qui permettent de lire des messages sans les consommer.
Ce bag est accessible dans Twig via app.flashes
:
{% for message in app.flashes('notice') %}
<div class="flash-notice">
{{ message }}
</div>
{% endfor %}
Symfony fournit deux contrôleurs spécifiques :
TemplateController
:
pour servir automatiquement un template comme page statique.RedirectController
:
pour rediriger automatiquement une URL vers une autre.
Il est possible de personnaliser les pages d'erreurs HTTP Symfony (ex: 404, 403, ...). Note : Pour tester le rendu d'une de ces pages d'erreur sans devoir causer l'erreur, on peut utiliser une URL spécifique (ex pour l'erreur 403 : localhost:8080/_error/403).
Publié le :
Cet article est une synthèse de la documentation officielle : Twig.
Twig est un gestionnaire de templates, des fichiers contenant du texte et des variables qui seront remplacées à terme, lorsqu'on "rendra" le template, par des valeurs.
C'est un langage écrit en PHP, générant du PHP. Les templates seront transpilés en PHP.
La fonction la plus courante est le echo()
de PHP, représentée par {{ }}
dans Twig, pour afficher une valeur.
Comme pour les autres composants, on peut lister ses options de paramétrage disponibles via la commande :
bin/console config:dump twig
Un bloc est une partie de template nommée et réutilisable. On peut le considérer comme une fonction qui affiche du contenu.
Par défaut, les blocs sont affichés/exécutés/rendus dès qu'ils sont présents dans un template. On peut toutefois en appeler un explicitement comme une fonction :
use
), à la manière d'un Trait PHP{# Bloc dans le même template, ou son parent #}
{{ block('my_block') }}
{# Blocs déclarés dans un autre template, mais considéré comme faisant partie du contexte #}
{% use 'a_template_with_several_blocks.twig' %}
{{ block('custom_block_1') }}
{{ block('custom_block_2') }}
{{ block('custom_block_3') }}
{# Bloc déclaré dans un autre template, lequel est indiqué explicitement #}
{{ block('my_external_block', 'another_template.twig') }}
Comme beaucoup de langage permettant de créer des fonctions dans d'autres fonctions, Twig permet de créer des blocs dans des blocs.
Les blocs sont comme des fonctions, mais n'ont pas d'argument. Les variables disponibles sont celles du contexte.
Une macro est comme un bloc sauf :
Exemple :
{% macro my_macro(text) %}
Some text: {{ text }}
{% endmacro %}
{# Appel de la macro présente dans le contexte #}
{{ _self.my_macro('some text') }}
{# Appel dune macro présente dans un autre template #}
{% import 'my_macros.twig' as my_alias %}
{{ my_alias.some_external_macro() }}
L'héritage de templates fonctionne comme l'héritage de classes. Les blocs sont alors comme des méthodes publiques, dont on hérite et que l'on peut surcharger.
Pour déclarer un template comme enfant d'un autre, on utilise le tag extends
(comme pour une classe PHP) :
{% extends 'base.html.twig' %}
{# Pour vider le contenu d'un bloc parent, on le redéclare en le laissant vide #}
{% block title %}
{% endblock %}
{# Pour remplacer le contenu d'un bloc parent, on le redéclare avec le nouveau contenu souhaité #}
{% block title %}
My child title
{% endblock %}
{# Pour conserver le contenu d'un bloc parent mais y ajouter du contenu #}
{% block title %}
My child title | {{ parent() }}
{% endblock %}
Remarque : Spécificité Twig, un template enfant ne doit jamais afficher du contenu hors des blocs définis par son parent.
Pour expliquer cela, on peut considérer le parent comme une interface. Seuls ses blocs (= ses méthodes) sont connues. Quand on manipule les templates (= les objets) qui l'implémentent, on n'a connaissance que de celles-ci.
À l'instar d'un trait PHP, un bloc de template externe inclus au contexte (via le tag use
) peut être surchargé.
On peut alors vider, remplacer, ou modifier le contenu du bloc (toujours via la fonction parent()
) .
Une variable globale est disponible partout dans Twig, à tout moment. La liste de ces variables est visible avec la commande suivante :
bin/console debug:twig
Remarque : ces variables peuvent être surchargées par une variable locale au template (si elles ont le même nom).
Twig en fournit 3 par défaut :
_self
: le nom du template courant_context
: la liste des variables (locales) disponibles (celles passées dans le render()
côté PHP)_charset
: le charset courantSymfony (ou plutôt son Twig Bridge) en ajoute une pour nous : app
(documentation).
On peut en ajouter
# config/packages/twig.yaml
twig:
globals:
my_var: 'value'
my_param1: '%some_parameter%'
my_service: '@my_service'
my_array_var:
- 'Monday'
- 'Tuesday'
GlobalInterface
et sa méthode getGlobals()
.Pour inclure un template dans un autre, on utilise le tag include
, ou la fonction include()
:
{# Avec tout le contexte courant #}
{% include 'template.twig' %}
{{ include('template.twig') }}
{# Avec le contexte courant et des variables spécifiques #}
{% include 'template.twig' with {'my_var': 'some_value'} %}
{{ include('template.twig', {'my_var': 'some_value'}) }}
{# Sans le contexte courant #}
{% include 'template.twig' only %}
{{ include('template.twig', with_context = false) }}
{# Sans le contexte courant mais des variables spécifiques #}
{% include 'template.twig' with {'my_var': 'some_value'} only %}
{{ include('template.twig', {'my_var': 'some_value'}, with_context = false) }}
Il est considéré comme une bonne pratique de retirer le contexte courant lors de l'inclusion
L'avantage d'utiliser le tag, est de pouvoir facilement basculer vers embed
si on a besoin de plus de flexibilité
(cf. ci-après).
Pour modifier l'un des blocs du contenu inclus, il faut utiliser le tag embed
.
{# Avec tout le contexte courant #}
{% embed 'template.twig' %}
{% block my_block %}
This block has been overrided!
{% endblock %}
{% endembed %}
On peut retirer le contexte pour embed
de la même manière que include
.
La distinction entre les deux est la même que dans le monde bash Linux.
Un filtre est une fonction, recevant au moins un argument.
{{ some_function() }}
{{ some_function_with_args('arg1', 'arg2') }}
{{ 'a string as argument'|some_filter }}
{{ 'a string as argument'|some_filter_with_more_args('arg2') }}
Les filtres permettent de chainer facilement plusieurs traitements sans que la variable initiale soit perdue dans la notation :
{# avec filtres, on lit de gauche à droite #}
{{ 'mon texte'|gras|italique|color('red')|majuscule }}
{# avec fonctions, on lit de l'intérieur vers l'extérieur #}
{{ majuscule(color(italique(gras('mon texte)), 'red')) }}
Ces opérateurs ajoutent du sucre syntaxique pour les if
des templates.
Exemples (in
, is
, matches
, starts
, end
) :
{% if 'monday' in my_day_list %}{% endif %}
{% if 'a' in my_string %}{% endif %}
{% if my_var is defined %}{% endif %}
{% if my_var matches '/some_regex/' %}{% endif %}
{% if my_var starts with 'my_prefix' %}{% endif %}
{% if my_var not ends with 'my_suffix' %}{% endif %}
Il est possible d'en ajouter dans une extension twig, en implémentant la méthode getOperators()
.
Les tests ajoute du sucre syntaxique pour les if
des templates contenant l'opérateur logique is
.
Exemples (defined
, empty
, iterable
, odd
) :
{% if my_var is defined %}{% endif %}
{% if my_var is empty %}{% endif %}
{% if my_var is iterable %}{% endif %}
{% if my_var is odd %}{% endif %}
{% if my_var is sameas(some_var) %}{% endif %}
Il est possible d'en ajouter dans une extension twig, en implémentant la méthode getTests()
.
Une extension Twig permet d'ajouter des filtres et fonctions personnalisés, mais également des tags, des opérateurs logiques, des tests, ...
Les filtres (TwigFilter
) et fonctions (TwigFunction
) que l'on déclare sont des objets qui associent un nom à un
Callable. Lorsqu'on utilisera ce nom dans un template, le Callable sera appelé.
Remarque : par défaut, toutes fonctions ou filtres ajoutés verra sa valeur de retour échappée.
Pour éviter cela, on peut indiquer ['is_safe' => ['html']]
par exemple lors de leur déclaration.
Si une extension a besoin d'un service pour le traitement de l'un de ses filtres ou fonctions, il faut éviter d'ajouter cette dépendance à l'extension, par soucis de performance.
En effet, si on l'injecte dans le constructeur de l'extension, il sera instancié très tôt durant l'exécution : dès l'enregistrement de l'extension.
Pour éviter ça, on peut déplacer le code nécessitant la dépendance dans un service dédié,
implémentant TwigE\Extension\RuntimeExtensionInterface
.
On utilisera ensuite le nom de ce service comme "instance" de Callable, et twig l'instanciera automatiquement au bon moment :
use \Twig\TwigFilter;
use \App\Twig\MyServiceRuntime;
public function getFilters(): array {
return [
new TwigFilter('my_filter', [MyServiceRuntime::class, 'methodOfServiceToCall']),
];
}
Le service ne sera instancié qu'à l'appel du filtre ou de la fonction, et pas avant. Il est donc logique de créer un service par dépendance.
La convention utilisée par les extensions du Twig Bridge est de les ranger sous le namespace \App\Twig
avec les extensions, et de les suffixer avec Runtime
.
L'échappement permet à du texte de ne pas être interprêté par un langage.
Dans Twig, par défaut toute variable affichée (ex: {{ my_variable }}
) est échappée.
Toutes ces syntaxes sont équivalentes :
{{ my_var }}
{{ my_var|e }}
{{ my_var|escape }}
{{ my_var|escape('html') }}
Explication : e
est un alias au filtre escape
, qui reçoit par défaut la valeur html
.
Le filtre escape
accepte les valeurs suivantes en argument : html
, js
, css
, url
, html_attr
.
Le dernier permet d'échapper les attributs des balises HTML.
Le filtre raw
permet d'annuler l'échappement de la valeur.
{{ my_var|raw }}
Une autre technique consiste à changer l'extension du template twig.
Si votre template s'appelle my_template.js.twig
, alors escape
recevra js
pour valeur.
Si toutefois le filtre ne reconnais pas l'extension, html
sera choisi.
Imaginons qu'on souhaite afficher le nom de l'utilisateur courant en haut de chaque page de l'application.
Tous nos templates héritent d'un même parent qui contient un bloc affichant cette variable.
Pour que l'utilisateur courant soit passé au template, la solution de base consiste à l'ajouter dans le
tableau des variables lors du render()
. Cela implique par contre de devoir la passer pour les rendus de tous les
templates enfants et vient complexifier le contrôleur qui demande le rendu.
La solution consiste à créer un contrôleur dédié au rendu de cette variable uniquement, qui sera lui-même directement rendu dans le template :
{{ render(controller('App\\Controller\\MyController::myAction', {})) }}
Explications :
render()
appelle une URL et en affiche la réponsecontroller()
retourne la réponse du contrôleur en argumentUtiliser le rendu de contrôleur est considéré comme une bonne pratique.
On peut la privilégier pour chaque bloc du design mutualisé entre plusieurs pages.
L'inclusion de contrôleur permet de bénéficier du Cache HTTP via les fragments.
Pour cela, il faut remplacer render()
par render_esi()
, et activer son utilisation dans le framework.yaml
.
Pour traduire une chaîne, on peut utiliser le filtre trans
ou le bloc {% trans %}{% endtrans %}
.
Le premier échappe la chaîne.
Pour faire varier la traduction selon une variable (ex: le nombre ou le genre), on peut utiliser ICU.
Pour que le service de traduction utilise bien cette bibliothèque système, il faut ajouter le suffixe
+intl-icu
au nom du fichier de traduction (ex: messages+intl-icu.fr_FR.yaml
).
Depuis PH7.4 et l'arrivée des arrow functions, le filtre filter
accepte des fonctions anonymes :
{% for item in items|filter(i => i.relevent) %}
<p>{{ item.name }}</p>
{% else %}
<p>No item</p>
{% endfor %}
Le filtre format()
disponible dans les templates équivaut au sprintf()
de PHP.
{{ 'My string with %d word(s).'|format(5) }}
La fonction dump()
comme dans PHP affiche le contenu d'une variable (ou de toutes celles disponibles
si on l'appelle sans argument).
Pour les collecter mais ne pas les afficher dans le contexte (et seulement les voir via la debugtoolbar),
on peut utiliser le tag dump
à la place (ie. {% dump my_var %}
)
La fonction logout_path()
a été ajoutée pour générer une URL de logout directement.
Il faut ajouter la route dans le security.yaml
.
Beaucoup de fonctions ont été ajoutées pour formater une heure, une date, un nombre, ...
Elles sont toutes préfixées par format_
.
Elles sont fournies par IntlExtension
.
Publié le :
Cet article est une synthèse de la documentation officielle : Installing & Setting up the Symfony Framework.
Pour faciliter l'installation et le développement avec Symfony, téléchargez
l'exécutable symfony
.
La première fois que vous lancez exécutez symfony
, il vérifie automatiquement les prérequis
et vous propose de l'ajouter aux exécutables système.
L'installation consiste à initialiser un nouveau projet Symfony.
Deux choix principaux s'offrent à vous :
symfony new my_project_name --full
# Ou, sans l'exécutable, juste avec Composer
composer create-project symfony/website-skeleton my_project
symfony new my_project_name
# Ou, sans l'exécutable, juste avec Composer
composer create-project symfony/skeleton my_project
Si vous ne souhaitez pas la version courante de Symfony, vous pouvez en spécifier une autre :
symfony new my_project --version=4.4
# Ou, sans l'exécutable, juste avec Composer
composer create-project symfony/skeleton:"^4.4" my_project
Note : vous pouvez également remplacer le numéro de version par lts
ou next
.
Si vous récupérez un projet Symfony depuis un gestionnaire de version - au hasard, Git - il vous faut juste téléchargez les dépendances via Composer :
cd my_projects
git clone [...]
cd my_project/
composer install
Généralement, les configurations par défaut sont définies dans le fichier .env
, et
celles propres à l'environnement doivent être surchargées dans un fichier .env.local
(tous deux à la racine).
Dans ce dernier, on retrouvera uniquement les propriétés du premier que l'on souhaite modifier.
Typiquement, on y indiquera les informations de connexion à la base de données.
Les répertoires <my_project>/var/cache/
et <my_project>/var/log/
doivent être accessibles en écriture.
(Plus d'informations ici : Setting up or Fixing File Permissions.)
De plus, si vous souhaitez utiliser la console symfony (bin/console
) plus facilement, vous devez la rendre
exécutable :
cd my-project/
chmod +x bin/console
Remarque : Cela n'est pas nécessaire si vous l'utilisez à travers PHP (ex : php bin/console about
).
En phase de développement, on peut se passer d'un serveur web classique comme apache ou nginx.
À la place, l'exécutable symfony
peut en lancer un pour nous :
cd my-project/
symfony server:start
Plus d'informations ici : Symfony Local Web Server
Si vous préférez un serveur web classique, voici des exemples pour les configurer : Configuring a Web Server
Si vous souhaitez ajouter des composants Symfony prêts à l'emploi, vous pouvez
en récupérer les bundles avec Composer.
Symfony lui a ajouté le plugin Flex, pour vous éviter d'avoir à les configurer.
De plus, cela vous permet d'installer un composant sans savoir où le trouver. Par exemple, vous avez besoin d'un logger ? Lancez la commande :
cd my-project/
composer require logger
Vous souhaitez un débogueur en mode dev ? Lancez la commande :
cd my-project/
composer require --dev debug
Pour vérifier la présence de failles connues parmi les dépendances de votre projet,
symfony
propose une commande :
symfony check:security
Publié le :
C4 est une proposition d'organisation graphique pour représenter une architecture logicielle sous forme de schémas.
Documentation : https://c4model.com
Le premier principe propose d'utiliser différentes échelles, pour différents schémas, plutôt qu'un seul, tentaculaire et incompréhensible.
Les 4 échelles, de la plus générale à la plus détaillée constituent les 4C.
C'est le niveau le plus haut, le contexte : ce qui se passe autour de l'application.
Celle-ci est représentée en un bloc au centre, autour duquel gravitent :
C'est le niveau représentant les différentes briques logicielles de l'application : les conteneurs.
C'est un peu le même diagramme que le précédent, mais avec un zoom dans la boîte centrale.
Exemples de conteneurs :
Les composants représentent les différents besoins fonctionnels auxquels répond un conteneur.
Un schéma de ce niveau permet de visualiser l'organisation générale du conteneur.
Par exemple, on pourra trouver :
Cela correspond souvent aux premiers niveaux de namespace/package/répertoires/etc.
C'est le niveau qui zoome sur l'un des composant. Il est beaucoup plus anecdotique que les précédents, et pourrait représenter un diagramme de classes, par exemple.
Le but du modèle n'est pas d'imposer un nombre d'échelles. De toute façon, et particulièrement en informatique, tout est poupées russes. On peut toujours zoomer et créer un nouveau schéma augmentant le niveau de détails.
Le but est plutôt de savoir découper ses schémas pour qu'ils restent pertinents et lisibles.
C'est ainsi seulement, qu'ils ajoutent de la valeur.
Le second aspect important du modèle soutien ce même objectif. On peut le résumer ainsi : un schéma doit se suffire à lui-même.
Il doit donc contenir suffisamment de texte (dans les blocs et sur leurs liaisons), un titre et une légende.
La comparaison avec une carte de géographie est très bien trouvée, car c'est exactement la même méthode. Une carte doit montrer quelque chose, sans besoin d'une page d'explication à côté.
Pour faciliter la création de schémas, le site officiel recense plusieurs outils, dont :
Publié le :
TortoiseSVN permet de générer des patchs pour passer d'une version à une autre du projet.
Il y a deux types de patch :
Le premier type génère un fichier .patch
, qui ressemble à quelque chose comme ça :
Index: settings/siteaccess/bo/site.ini.append.php
===================================================================
--- settings/siteaccess/bo/site.ini.append.php (révision 864)
+++ settings/siteaccess/bo/site.ini.append.php (copie de travail)
@@ -37,4 +37,23 @@
CachedViewPreferences[full]=admin_navigation_content=1;admin_children_viewmode=list;admin_list_limit=1
TranslationList=
+[DebugSettings]
+DebugOutput=enabled
+DebugToolbar=enabled
+DebugRedirection=disabled
+
+[TemplateSettings]
+Debug=disabled
+
+[DatabaseSettings]
+SQLOutput=disabled
+
*/ ?>
\ No newline at end of file
On peut y voir pour chaque fichier modifié, les lignes ajoutées et supprimées depuis le dernier commit.
Le second type génère l'arborescence et les fichiers entiers.
La liste des fichiers modifiés apparait, et on peut voir pour chacun d'eux les lignes impactées. On peut alors choisir de patcher tous les fichiers ou seulement certains.
La liste des fichiers modifiés entre les deux versions apparaît. Il suffit alors de :
Publié le :
Si vous intégrez une Google Map mais qu'elle reste vide, vérifiez bien :
Voici l'exemple fourni par Google dans la documentation.
Modifié le :
Publié le :
Lors de l'installation de Git sous Windows, vous avez la possibilité d'activer un menu contextuel proposant des raccourcis vers les tâches les plus courantes de Git (commit, pull, ...).
Le problème c'est que TortoiseGit propose la même fonctionnalité en mieux. Si vous utilisez ces deux logiciels, vous obtenez donc deux menus contextuels identiques...
Pour supprimer celui créé par Git, utilisez une des commandes suivantes.
Pour Windows 32 bits :
cd C:\Program Files\Git\git-cheetah
regsvr32 /u git_shell_ext.dll
Pour Windows 64 bits :
cd C:\Program Files\Git\git-cheetah
regsvr32 /u git_shell_ext64dll
Publié le :
Lorsque docker crée des réseaux virtuels entre ses conteneurs, il utilise des plages IP.
En général pas de problème, il utilise des plages non couramment utilisées.
Par contre, si ses plages habituelles ne sont pas disponibles (ou pour d'autres raisons ?),
il est possible qu'il en utilise une autre, par exemple 192.168.x.x
.
Cela peut alors être problématique, surtout si vous utilisez un VPN ou des ressources réseaux en parallèle,
car cette plage est classiquement utilisée.
On peut alors avoir un conflit d'adressage et les ressources réseaux ou du VPN peuvent ne plus être accessibles.
Pour corriger ça, une première approche consiste à killer le réseau (créé par docker) qui pose problème puis à le recréer.
Pour cela :
# Listage des réseaux créés par docker
docker network list
# Identifiez le réseau correspondant au conteneur qui a causé le problème (copiez son ID)
# Pour vérifier la plage IP qu'il utilise :
docker network inspect <ID>
# Suppression du réseau qui pose problème
docker network rm <ID>
Si cela ne suffit pas, essayez la suite de la procédure proposée ici.
Publié le :
Selon la langue, différents caractères d'espace peuvent être utilisés.
En français par exemple (même si les claviers ne nous en facilitent pas l'écriture), l'espace entre des guillemets et
le mot qu'ils encadrent n'est normalement pas d'une taille normale. Même chose avant :
.
Les éditeurs de traitement de texte (comme LibreOffice ou Word) connaissent mieux que nous cette norme d'écriture et
font le remplacement automatiquement.
Quand on récupère une chaîne qui contient de tels espaces, cela peut poser des problèmes d'affichage (ex: génération d'un PDF).
Voici la liste de ces caractères spéciaux, pour PHP :
$spaceUnicodeChars = [
'\x{0020}',
'\x{00A0}',
'\x{180E}',
'\x{2000}',
'\x{2001}',
'\x{2002}',
'\x{2003}',
'\x{2004}',
'\x{2005}',
'\x{2006}',
'\x{2007}',
'\x{2008}',
'\x{2009}',
'\x{200A}',
'\x{200B}',
'\x{202F}',
'\x{205F}',
'\x{3000}',
'\x{FEFF}',
];
Pour javascript :
const SPACE_UNICODE_CHARS = [
'\u{0020}',
'\u{00A0}',
'\u{180E}',
'\u{2000}',
'\u{2001}',
'\u{2002}',
'\u{2003}',
'\u{2004}',
'\u{2005}',
'\u{2006}',
'\u{2007}',
'\u{2008}',
'\u{2009}',
'\u{200A}',
'\u{200B}',
'\u{202F}',
'\u{205F}',
'\u{3000}',
'\u{FEFF}',
];
Publié le :
Pour lancer Composer sans la limite de mémoire imposée par la configuration de PHP, on peut utiliser cette commande :
php -d memory_limit=-1 $(which composer) update
Explications :
memory_limit
(-1
pour infinie)which
est une commande système qui affiche le chemin complet vers un exécutable (ici composer
)Modifié le :
Publié le :
Pendant le lancement du système, des services sont démarrés.
L'ordre de lancement a une importance, car beaucoup de services ont besoin d'autres services pour fonctionner.
Sous linux il y a donc la notion de runlevel (cf. wikipédia). Les runlevels (niveau d'exécution) vont de 0 à 6 et se font un à un dans l'ordre croissant.
Il arrive qu'une erreur se produise lors du lancement d'un service à un certain runlevel, bloquant ainsi le démarrage.
Il est possible de forcer le passage au niveau suivant, même si tout n'est pas encore terminé.
Pour cela appuyez sur les touches suivantes durant l'initialisation : CTRL
+ ALT
+ F<NIVEAU>
Avec <NIVEAU>
le niveau de runlevel vers lesquels passer (ex: CTRL
+ ALT
+ F4
pour passer au niveau 4).
Publié le :
Le query builder de doctrine protège automatiquement des injections SQL, car elle utilise des requêtes préparées où sont injectés les paramètres :
// Utilisateurs dont le nom est 'Toto'
$userName = 'Toto';
->where('u.name = :userName')
->setParameter('userName', $userName);
Par contre si on utilise un LIKE
pour retourner tous les utilisateurs dont le nom commence par :
// Utilisateurs dont le nom commence par 'To'
$prefix = 'To';
->where('u.name LIKE :userNameStart')
->setParameter('userNameStart', '%'.$prefix);
Si $prefix
est une donnée provenant d'un formulaire, on est pas à l'abri qu'elle ne contienne pas d'autres wildcards (_
ou %
).
Il est préférable de l'échapper :
// Utilisateurs dont le nom commence par 'Toto'
$prefix = 'To';
->where('u.name LIKE :userNameStart')
->setParameter('userNameStart', '%'.addcslashes($prefix, '%_'));
Publié le :
Si vous utilisez l'authentification via Ldap de Symfony 4, avec les composants symfony/ldap
et symfony/security-bundle
voici les 3 étapes qui se jouent en arrière plan :
Selon l'étape, différentes classes et paramètres du framework seront utilisés.
Les deux premières étapes ont lieu au même endroit : dans la méthode loadUserByUsername()
du LdapUserProvider (Symfony\Component\Security\Core\User\LdapUserProvider
).
public function loadUserByUsername($username)
{
try {
// Etape 1
$this->ldap->bind($this->searchDn, $this->searchPassword);
// Etape 2
$username = $this->ldap->escape($username, '', LdapInterface::ESCAPE_FILTER);
$query = str_replace('{username}', $username, $this->defaultSearch);
$search = $this->ldap->query($this->baseDn, $query);
} catch (ConnectionException $e) {
throw new UsernameNotFoundException(sprintf('User "%s" not found.', $username), 0, $e);
}
$entries = $search->execute();
$count = \count($entries);
if (!$count) {
throw new UsernameNotFoundException(sprintf('User "%s" not found.', $username));
}
if ($count > 1) {
throw new UsernameNotFoundException('More than one user found');
}
$entry = $entries[0];
try {
if (null !== $this->uidKey) {
$username = $this->getAttributeValue($entry, $this->uidKey);
}
} catch (InvalidArgumentException $e) {
}
return $this->loadUser($username, $entry);
}
Remarque :
Vous pouvez surcharger ce comportement en créant un UserProvider
héritant de LdapUserProvider
.
Il faut alors le déclarer dans le config/security.yaml
, et l'affecter à un firewall :
security:
providers:
# Je déclare mon provider spécifique
my_ldap_provider:
id: App\Security\MyLdapUserProvider
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
api:
pattern: ^/
stateless: true
anonymous: true
# J'indique que je souhaite utiliser le provider déclaré en haut
provider: my_ldap_provider
json_login_ldap:
service: Symfony\Component\Ldap\Ldap
dn_string: '%env(resolve:LDAP_SEARCH_DN_FOR_BIND)%'
check_path: api_login
success_handler: lexik_jwt_authentication.handler.authentication_success
failure_handler: lexik_jwt_authentication.handler.authentication_failure
require_previous_session: false
guard:
authenticators:
- lexik_jwt_authentication.jwt_token_authenticator
C'est l'instruction $this->ldap->bind($this->searchDn, $this->searchPassword);
qui se charge de l'authentification.
Les arguments de la méthode sont récupérés dans les paramètres yaml du config/services.yaml
:
parameters:
# Alimentera $this->searchDn (par exemple "cn=Admin User,dc=mycompany,dc=local")
ldap.search_dn: '%env(resolve:LDAP_SEARCH_DN)%'
# Alimentera $this->searchPassword (par exemple "m0tDeP4sseAdm1n")
ldap.search_password: '%env(resolve:LDAP_SEARCH_PASSWORD)%'
Explications :
Admin User
correspond à l'identifiant d'un utilisateur ayant le droit d'accéder au Ldap, dont le mot de passe est m0tDeP4sseAdm1n
.Remarque : Les valeurs de ces paramètres sont stockées dans le .env
(ou .env.local
). Elles sont récupérées via le %env(resolve:XXX)%
.
C'est l'instruction $search = $this->ldap->query($this->baseDn, $query);
qui s'en charge.
La valeur de $this->baseDn
provient à nouveau du fichier config/services.yaml
:
parameters:
# Alimentera $this->baseDn (à priori la même chose que pour le searchDn, mais sans le nom d'utilisateur
# par exemple "dc=mycompany,dc=local")
ldap.base_dn: '%env(resolve:LDAP_BASE_DN)%'
La valeur de la $query
est un "filtre" Ldap pour trouver l'utilisateur. Il contiendra à priori son identifiant unique. Par exemple (uid=jl.david@mycompany.com)
.
Cette fois ça se passe dans la méthode checkAuthentication()
de la classe LdapBindAuthenticationProvider
(Symfony\Component\Security\Core\Authentication\Provider
) :
protected function checkAuthentication(UserInterface $user, UsernamePasswordToken $token)
{
$username = $token->getUsername();
$password = $token->getCredentials();
if ('' === (string) $password) {
throw new BadCredentialsException('The presented password must not be empty.');
}
try {
$username = $this->ldap->escape($username, '', LdapInterface::ESCAPE_DN);
if ($this->queryString) {
$query = str_replace('{username}', $username, $this->queryString);
$result = $this->ldap->query($this->dnString, $query)->execute();
if (1 !== $result->count()) {
throw new BadCredentialsException('The presented username is invalid.');
}
$dn = $result[0]->getDn();
} else {
$dn = str_replace('{username}', $username, $this->dnString);
}
// Etape 3
$this->ldap->bind($dn, $password);
} catch (ConnectionException $e) {
throw new BadCredentialsException('The presented password is invalid.');
}
}
Comme pour l'authentification, c'est l'instruction $this->ldap->bind($dn, $password);
qui est utilisée.
Cette fois le DN contiendra le nom de l'utilisateur à authentifier et $password
son mot de passe.
Il est configuré dans le fichier config/security.yaml
:
security:
api:
json_login_ldap:
# Alimentera $this->dnString (par exemple "uid={username},ou=SomeRandomOU,dc=mycompany,dc=local")
dn_string: '%env(resolve:LDAP_SEARCH_DN_FOR_BIND)%'
# Il est aussi possible d'utiliser un query_string
#query_string: '%env(resolve:LDAP_SEARCH_QUERY_STRING)%'
Le {username}
sera remplacé par le nom de l'utilisateur recherché.
Modifié le :
Publié le :
Il n'est pas possible de préciser le schéma à utiliser dans l'url de connexion à la base de données PostgreSQL :
DATABASE_URL=pgsql://my_user:my_pwd@localhost:5432/my_db
Par défaut, c'est le schéma public
de postgres qui est utilisé.
Pour en changer, il faut exécuter cette commande SQL sur la base de données :
ALTER USER my_user SET search_path = my_custom_schema;
Modifié le :
Publié le :
Pour supprimer tous les conteneurs docker :
docker rm $(docker ps -a -q)
Pour supprimer toutes les images docker :
docker rmi $(docker images -q)
Pour supprimer tous les volumes docker :
docker volume prune
Publié le :
Lorsqu'on utilise JWT pour l'authentification dans API Platform, on a besoin d'ajouter ce jeton à nos requêtes pour communiquer avec l'API. API Platform propose un bouton pour faire ça facilement dans Swagger :
Ce bouton n'apparaît pas par défaut. Il faut l'activer dans la configuration d'API Platform :
# config/package/api_platform.yaml
api_platform:
swagger:
api_keys:
apiKey:
name: Authorization
type: header
Modifié le :
Publié le :
Si vous utilisez JWT pour gérer l'authentification entre le backend Symfony et un front autre (ex: Angular), il peut arriver que le jeton "se perde" entre les deux.
Par exemple :
AbstractController::getUser()
) mais il est null
Si vous utilisez PHP-FPM avec Apache le problème peut venir du passage des headers entre les deux. Symfony préconise d'ajouter la ligne suivante dans le Virtual host de votre application :
SetEnvIfNoCase ^Authorization$ "(.+)" HTTP_AUTHORIZATION=$1
Pour vérifier si vous avez ce symptôme, il suffit de consulter le profiler Symfony. Parmi les headers de la partie Request / Response, vous devriez trouver le jeton JWT (comme c'est le cas dans la console de votre navigateur).
Publié le :
Voici trois outils utiles pour auditer du code PHP de manière statique :
cd my_project
wget https://github.com/phpstan/phpstan-shim/raw/master/phpstan.phar
php phpstan analyse src -l 0
Explications :
src
est le répertoire à analyser. Plusieurs peuvent être passés, séparés par des espaces.-l
permet de choisir le niveau d'acceptation des erreurs, avec 0
le moins strict possible, et 7
pour le plus strict.Créez un fichier sonar-project.properties
à la racine de votre projet, contenant les lignes suivantes :
sonar.projectKey=my:project
sonar.projectName=My project
sonar.projectVersion=1.0
sonar.sources=src
sonar.sourceEncoding=UTF-8
Explication : src
est le répertoire à analyser
Remarques :
sonar.exclusions
Lancez SonarQube via Docker :
docker pull sonarqube
docker run -d --name sonarqubedocker_sonarqube_1 -p 9000:9000 sonarqube
admin
/admin
).Lancez Sonar runner avec Docker, pour exécuter l'analyse :
docker run --link sonarqubedocker_sonarqube_1:sonarqube \
--entrypoint /opt/sonar-runner-2.4/bin/sonar-runner \
-e SONAR_USER_HOME=/data/.sonar-cache \
-v $(pwd):/data -u $(id -u) sebp/sonar-runner \
-Dsonar.host.url=http://sonarqube:9000 \
-Dsonar.jdbc.url=jdbc:h2:tcp://sonarqube/sonar \
-Dsonar.jdbc.username=sonar \
-Dsonar.jdbc.password=sonar \
-Dsonar.jdbc.driverClassName=org.h2.Driver \
-Dsonar.embeddedDatabase.port=9092
L'application SonarQube affiche maintenant le rapport d'analyse de votre pojet.
Voici la page d'accueil du rapport, à partir de laquelle on accède aux alertes remontées et au code associé.
PHPStorm affiche des alertes directement dans le code, en fonction des réglages utilisés. Ceux-ci peuvent être personnalisés par projet ou au niveau global, via Settings > Editor > Inspections.
L'IDE propose aussi une vue d'ensemble de tout le projet.
Pour l'utiliser, faites un clic-droit sur le répertoire à scanner (ex src
), puis cliquez sur Inspect code...
Le rapport peut ensuite être exporté au format HTML ou XML.
Modifié le :
Publié le :
PHPUnit fournit des outils pour mocker des classes et leurs méthodes. Ex :
$valueINeed = 'my_value';
$myMockedObject = $this->createMock(MyClassToMock::class);
$myMockedObject->method('myMethodToMock')
->willReturn($valueINeed);
Ce n'est pas le cas pour les fonctions natives, comme time()
ou random_bytes()
.
C'est problématique si vous voulez tester une méthode comme celle-ci :
// src/Generator/TokenGenerator.php
namespace App\Generator;
use App\Exception\TokenGenerationException;
class TokenGenerator
{
public function generateBase64Token(int $byteLength): string
{
if ($byteLength <= 0) {
throw new \InvalidArgumentException('The length must be greater than zero');
}
try {
$token = random_bytes($byteLength);
} catch(\Exception $e) {
throw new TokenGenerationException('An unexpected error has occured');
}
return base64_encode($token);
}
}
À la place de mocker la fonction, vous pouvez la redéfinir à votre convenance, dans le namespace où vous en avez besoin.
Par exemple :
namespace App\Generator;
function random_bytes(int $length) {
return 'fake_random_string';
}
Lors de l'exécution, PHP cherchera la fonction dans le namespace courant, puis dans le namespace racine /
s'il ne la trouve pas.
(À noter que si vous utilisez \random_bytes()
, PHP cherchera alors uniquement dans le namespace racine.)
On peut ainsi tester la méthode correctement :
// tests/What/Ever/Probably/Just/App/Generator/TokenGeneratorTest.php
namespace App\Generator {
function random_bytes(int $length) {
return 'fake_random_string';
}
}
namespace What\Ever\Probably\Just\App\Generator {
use App\Generator\TokenGenerator;
use PHPUnit\Framework\TestCase;
class TokenGeneratorTest extends TestCase
{
/** @var TokenGenerator */
private $tokenGenerator;
public function testGenerateBase64Token(): void
{
$validLength = 128;
$generated = $this->tokenGenerator->generateBase64Token($validLength);
$expected = 'ZmFrZV9yYW5kb21fc3RyaW5n';
$this->assertEquals($expected, $generated);
}
public function testGenerateBase64TokenWithInvalidLength(): void
{
$invalidLength = -12;
$this->expectException(\InvalidArgumentException::class);
$this->tokenGenerator->generateBase64Token($invalidLength);
}
protected function setUp(): void
{
parent::setUp();
$this->tokenGenerator = new TokenGenerator();
}
}
}
Remarque : Quand il y a plusieurs namespace utilisés dans un même fichier PHP,
on précise le code concerné en l'encadrant par des {}
.
Imaginons maintenant qu'on veuille tester le cas où la fonction random_bytes()
lève une exception,
puisque la documentation nous informe que cela peut arriver (en cas de problème mémoire).
Il faut donc que notre "fonction de surcharge" puisse retourner notre valeur "bouchon" ou lever une exception.
La difficulté c'est qu'on souhaite sans modifier sa signature, qu'elle ait un comportement différent
selon le cas de test où on l'appelle.
Pour ça il y a une astuce (assez sale) : utiliser une variable globale pour indiquer à la fonction le comportement à suivre.
On peut ainsi modifier notre fonction de surcharge et implémenter un 3e cas de test :
// tests/What/Ever/Probably/Just/App/TokenGeneratorTest.php
namespace {
$randomBytesFuncThrowsException = false;
}
namespace App\Generator {
function random_bytes(int $length) {
global $randomBytesFuncThrowsException;
if (isset($randomBytesFuncThrowsException) && $randomBytesFuncThrowsException === true) {
throw new \Exception();
}
return 'fake_random_string';
}
}
namespace What\Ever\Probably\Just\App\Generator {
use App\Exception\TokenGenerationException;
use App\Generator\TokenGenerator;
use PHPUnit\Framework\TestCase;
class TokenGeneratorTest extends TestCase
{
// [...]
public function testGenerateBase64TokenWithUnexpectedError(): void
{
global $randomBytesFuncThrowsException;
$randomBytesFuncThrowsException = true;
$noMatterWhatLength = 666;
$this->expectException(TokenGenerationException::class);
$this->tokenGenerator->generateBase64Token($noMatterWhatLength);
}
// [...]
}
}
Remarque : Pour déclarer une variable globale, on se place dans le namespace racine.
Un autre cas courant et problématique est le constructeur DateTime()
.
Quand on veut la date courante, on l'utilise souvent sans argument et là encore, pas moyen de mocker l'objet généré. À chaque test l'heure aura un peu avancé..
La solution toute simple consiste à utiliser le premier argument du constructeur :
// Remplacer
$dateTime = new \DateTime();
// par
$dateTime = new \DateTime(sprintf('@%s', time()));
On peut alors utiliser la technique présentée plus haut, et surcharger la fonction time()
:
namespace Here\I\Instanciate\Datetime {
function time()
{
return 1557481811;
}
}
namespace Some\Where {
class SomethingWhichDealsWithDateTest extends KernelTestCase
{
private $someServiceWhichInstanciatesDateTime;
public function testMethodWhichInstanciateDatetime(): void
{
$this->someServiceWhichInstanciatesDateTime->getDatetimeInstance();
// TODO: test something
}
}
// [...]
}
Modifié le :
Publié le :
Sous Linux, vous pouvez utiliser la commande suivante pour savoir quel processus utilise un port :
# Pour le port 80
netstat -tlnp | grep 80
Cela retournera par exemple :
tcp6 0 0 :::80 :::* LISTEN 32198/apache2
L'ID du processus étant ici 32198
.
Remarque :
Pour voir tous les processus, il sera peut-être nécessaire de lancer la commande en tant que root
(ou avec sudo
).
Modifié le :
Publié le :
PHP 7 introduit 2 nouveaux opérateurs : ??
et <=>
, nommés respectivement Null coalescent et Spaceship.
Cet opérateur permet de simplifier l'affectation des valeurs par défaut via les opérateurs ternaires :
$value = (isset($x) && $x !== null) ? $x : $defaultValue;
peut maintenant s'écrire :
$value = $x ?? $defaultValue;
Comme l'indique la documentation, il permet de vérifier si une variable existe et est non nulle.
Remarque :
Il ne faut pas le confondre avec l'opérateur ternaire "abrégé" (depuis PHP 5.3), à savoir ?:
.
Celui-ci permet juste d'omettre la partie centrale, sans vérifier l'existence de la variable.
$value = $x ?: $valueIfXIsFalse;
$value
devient $x
si $x
est considéré comme "vrai" . $value
devient $valueIfXIsFalse
si $x
est considéré comme "faux".
Contrairement au null coalescent, il reverra un warning si $x
n'est pas défini.
Cet opérateur permet de simplifier la comparaison entre deux variables :
$comparison = ($a < $b) ? -1 : (($a > $b) ? 1 : 0);
peut maintenant s'écrire :
$comparison = $a <=> $b;
Comme l'indique la documentation,
cet opérateur retourne donc -1
, 1
ou 0
selon la différence entre $a
et $b
.
Publié le :
Parmi les méthodes magiques
disponibles en PHP, la méthode __invoke()
permet de "transformer un objet en fonction".
Ainsi, voici l'exemple donné dans la documentation :
<?php
class CallableClass
{
public function __invoke($x)
{
var_dump($x);
}
}
$obj = new CallableClass;
$obj(5);
var_dump(is_callable($obj));
?>
Ce qui affiche :
int(5)
bool(true)
Explication :
On peut appeler l'instance de notre classe comme une fonction.
C'est alors la méthode __invoke()
qui est exécutée.
Dans certains languages, comme le javascript, les fonctions sont des Objets. On appelle ce principe "first-class". C'est parfois utile d'avoir une fonction sous forme d'objet à manipuler. Selon telle ou telle propriété, elle pourra s'exécuter de manières différentes.
Voici un cas d'usage proposé sur stackoverflow.com :
Imaginons qu'on veuille trier ce tableau :
$arr = [
['key' => 3, 'value' => 10, 'weight' => 100],
['key' => 5, 'value' => 10, 'weight' => 50],
['key' => 2, 'value' => 3, 'weight' => 0],
['key' => 4, 'value' => 2, 'weight' => 400],
['key' => 1, 'value' => 9, 'weight' => 150]
];
Via la fonction usort()
, on peut trier facilement en fonction de la clé value
:
$comparisonFn = function($a, $b) {
return $a['value'] < $b['value'] ? -1 : ($a['value'] > $b['value'] ? 1 : 0);
};
usort($arr, $comparisonFn);
Maintenant si on souhaite trier en fonction de la clé weight
:
usort($arr, function($a, $b) {
return $a['weight'] < $b['weight'] ? -1 : ($a['weight'] > $b['weight'] ? 1 : 0);
});
La logique de la fonction est exactement la même que précédemment, mais on ne peut pas la réutiliser.
À la place, on peut créer une classe avec la méthode __invoke()
:
class Comparator {
protected $key;
public function __construct($key) {
$this->key = $key;
}
public function __invoke($a, $b) {
return $a[$this->key] < $b[$this->key] ? -1 : ($a[$this->key] > $b[$this->key] ? 1 : 0);
}
}
et ainsi choisir au moment de l'appel :
usort($arr, new Comparator('key')); // tri par 'key'
usort($arr, new Comparator('value')); // tri par 'value'
usort($arr, new Comparator('weight')); // tri par 'weight'
Source : stackoverflow.com
Publié le :
Pour faire ses premiers pas avec NodeJS, le site nodeschool.io propose des ateliers de développement.
Concrètement, il s'agit de petites applications NodeJS qui affichent un énoncé, et sont capables d'éxécuter votre programme pour vérifier qu'il correspond aux consignes. L'un des ateliers utiles pour débuter s'appelle learnyounode.
Pour l'utiliser il vous faut NodeJS :
curl -sL https://deb.nodesource.com/setup_8.x | sudo -E bash -
sudo apt-get install -y nodejs
Ensuite ça se passe avec npm (inclus vec NodeJS) :
npm install -g learnyounode
learnyounode
Le framework Express semble très largement répandu dans l'univers de NodeJS, dès lors qu'on souhaite créer une application web (API REST, site front, ...).
Mozilla Developpeur propose un bon tutoriel pour démarrer avec Express.
Il s'agît d'un cas concret de création d'appplication web, pour gérer une bibliothèque (de livres).
Une partie front interroge un back REST full qui stocke les données dans une base MongoDB.
(Il y a également un atelier sur le sujet proposé sur nodeschool.io)
Pour utiliser une base MongoDB avec NodeJS, il existe l'API Mongoose.
(Elle est utilisée dans le tutoriel précédent.)
Modifié le :
Publié le :
Lorsqu'on fait un appel à un webservice ou qu'on exécute une grosse requête en base de données, cela prend un certain temps. On sait ce qu'on est sensé récupérer mais pas quand, ce qui semble plutôt gênant pour lancer la suite du traitement (ex: afficher les résultats).
Ce qu'on appelle une promesse, c'est ce qu'on espère recevoir comme résultat (un tableau, un flux, un objet, ... ce qu'on veut). On code la suite du traitement à réaliser comme si on avait ce résultat, sauf qu'on part de l'objet Promise à la place.
Si on n'utilise pas les promesses et qu'on utilise une fonction asynchrone, cela peut ressembler à ça :
function getNumberFromWebservice(successCllback, errorCallback) {
try {
// On récupère les données
let number = xxx();
successCllback(number);
} catch {
errorCallback('Impossible de récupérer le nombre.');
}
}
getResultsFromWebservice(function (number) {
console.log('Affichage du résultat :');
console.log(number);
}, function (error) {
console.error(error);
});
Cette méthode fonctionne bien, mais à quoi cela ressemblerait-il si on appelait une seconde fonction asynchrone au retour de la première ?
function getNumberFromWebservice1(successCllback, errorCallback) {
try {
// On récupère les données
let number = xxx();
successCllback(number);
} catch {
errorCallback('Impossible de récupérer le nombre.');
}
}
function getNumberFromWebservice2(number1, successCllback, errorCallback) {
try {
// On récupère les données
let number2 = yyy();
successCllback(number1 + number2);
} catch {
errorCallback('Impossible de récupérer le nombre.');
}
}
function displayResult(sum) {
console.log('Affichage de la somme :');
console.log(sum);
}
function displayError(error) {
console.error(error);
}
// /!\ Début de callback hell /!\
getResultsFromWebservice1(function (number1) {
getResultsFromWebservice2(number1, function (sum) {
displayResult(sum);
}, function (error) {
displayError(error);
});
}, function (error) {
displayError(error);
});
Ca devient vraiment peu lisible dès qu'on multiplie les fonctions asynchrones à chaîner. On a des callback de callback de callback (cf. Callback hell).
Les promesses permettent une syntaxe plus lisible :
// Pas de callback hell, on évite l'écriture en imbrication
getResultsFromWebservice1()
.then(function(number1) {
return getResultsFromWebservice2(number1);
}),
.then(function(sum) {
console.log('Affichage de la somme :');
console.log(sum);
}),
.catch(function (error) {
console.error(error);
});
// Ou même plus court avec la syntaxe allégée
getResultsFromWebservice1()
.then((number1) => getResultsFromWebservice2(number1)),
.then((sum) => displayResult(sum)),
.catch((error) => displayError(error));
Le principe est le même sauf qu'on n'a plus besoin de passer les callback en paramètres à chaque fonction. Par contre pour que ça marche, il faut modifier un peu chaque fonction pour qu'elle ne retourne non plus un nombre mais un objet Promise :
function getNumberFromWebservice1() {
return new Promise((resolve, reject) => {
// On récupère les données
let number = xxx();
resolve(number);
});
}
function getNumberFromWebservice2(number1) {
return new Promise((resolve, reject) => {
// On récupère les données
let number2 = yyy();
resolve(number1 + number2);
});
}
Remarque :
Les try/catch ont été retirés dans les fonctions. Cela veut dire qu'en a cas d'erreur levée par l'appel aux webservices (xxx() et yyy()),
ce sera le message de l'erreur qui sera affiché, et non plus le message 'Impossible de récupérer le nombre.'
.
Les promesses existent depuis un moment en javascript. Il y a pas mal de cas d'utilisation :
La plupart des framework proposent une couche supplémentaire pour gérer ces cas d'utilisation. Dans le monde de NodeJS par exemple, on utilise très souvent la lib async.
Documentation Mozilla Développeur :
Modifié le :
Publié le :
Sur le site de Bootstrap, voici le code pour afficher un bouton ouvrant une popin :
<button type="button" class="btn btn-primary" data-toggle="modal" data-target=".bs-example-modal-lg">Large modal</button>
<div class="modal fade bs-example-modal-lg" tabindex="-1" role="dialog" aria-labelledby="myLargeModalLabel">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
...
</div>
</div>
</div>
Par défaut la popin sera placée en haut de la fenêtre, ce qui n'est pas toujours très joli :
Si vous préférez que vos popin soient centrées verticalement, vous pouvez ajouter ces quelques lignes à votre feuille de style :
.modal.in .modal-dialog {
-webkit-transform: translate(0, calc(50vh - 50%));
-ms-transform: translate(0, 50vh) translate(0, -50%);
-o-transform: translate(0, calc(50vh - 50%));
transform: translate(0, 50vh) translate(0, -50%);
}
Vous obtenez alors ce résultat :
Publié le :
Pour authentifier programmatiquement un utilisateur en front-office, vous pouvez utiliser cette méthode :
<?php
use eZ\Publish\Core\MVC\Symfony\Security\User as SecurityUser;
use eZ\Publish\API\Repository\Values\User\User;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
/**
* Connecte l'utilisateur en argument.
*
* @param User $user
* Utilisateur eZ Publish récupéré depuis le repository
*
* @throws \Exception Si une erreur survient lors de la récupération du service de gestion de jeton.
*/
public function login(User $user)
{
// Authentification pour Symfony
$roles =['ROLE_USER'];
$security_user = new SecurityUser($user, $roles);
$security_user->setAPIUser($user);
$token = new UsernamePasswordToken($security_user, null, 'ezpublish_front', $roles);
$this->container->get('security.token_storage')->setToken($token);
// Authentification pour le repo eZ Publish
$this->repository->setCurrentUser($user);
}
Publié le :
Vous êtes sous Linux, ou vous n'avez pas Photoshop ? Vous avez besoin d'ouvrir correctement un fichier psd ?
Si c'est juste pour du découpage et pas de la créa, la version online fera très bien l'affaire !
C'est ce que propose le site https://www.photopea.com/.
Modifié le :
Publié le :
Si vous voulez accéder à votre application via un contexte spécifique, il faut ajouter cette ligne dans votre virtual host apache :
Alias /mon-contexte /chemin/vers/mon_appli/web
Si votre virtual host répond au nom de domaine www.mon-site.com
par exemple,
votre site sera maintenant accessible via l'URL www.mon-site.com/mon-contexte
.
Modifié le :
Publié le :
Lors de la mise à jour de Drupal (par exemple de 8.3.7 vers 8.4.3), vous pouvez rencontrer ce genre d'erreur :
$ drush updb -y
The following updates are pending:
views module :
Fix table names for revision metadata fields.
Do you wish to run all pending updates? (y/n): y
Après la mise à jour de views [ok]
Failed: InvalidArgumentException : The configuration property [error]
display.default.display_options.filters.my_custom_filter_id.value.2 doesn't
exist. dans Drupal\Core\Config\Schema\ArrayElement->get() (ligne 76 de
/var/www/ftvpro/web/core/lib/Drupal/Core/Config/Schema/ArrayElement.php).
Cache rebuild complete. [ok]
Finished performing updates.
Le module Views est incapable de mettre à jour la base de données correctement, à cause d'un filtre de vue.
Pour éviter ça, cherchez dans vos modules (ou ceux communautaires que vous utilisez) le filtre de vue en question.
(Ici my_custom_filter
.)
Le module qui déclare ce filtre doit également proposer une mise à jour de la base via un fichier yaml.
Pour cet exemple, le module my_module
doit contenir le fichier
config/schema/my_module.views.schema.yml
:
# Schema for the views plugins of the my_module module.
views.filter.my_custom_filter:
type: views.filter.in_operator
label: 'My custom view filter'
Si ce n'est pas le cas, crééz-le et relancer la commande drush updb
.
Remarque :
Si on regarde le fichier comment.views.schema.yml
que propose le module Comment,
on y trouve aussi ce genre de déclarations :
views.argument.argument_comment_user_uid:
type: views_argument
label: 'Commented user ID'
views.field.comment_depth:
type: views_field
label: 'Comment depth'
views.row.comment_rss:
type: views_row
label: 'Comment'
mapping:
view_mode:
type: string
label: 'Display type'
views.sort.comment_ces_last_comment_name:
type: views_sort
label: 'Last comment name'
Le problème peut donc sûrement se produire pour les arguments, les champs, les lignes et les tris des vues déclarés dans les modules.
Publié le :
Lorsqu'il y a un bug dans l'une de vos dépendances gérées par composer, il faut éviter de le corriger en modifiant directement la lib. Sinon, la correction sera automatiquement écrasée lors de la prochaine installation/mise à jour de la lib.
Si un patch est proposé sur le site du fournisseur de la lib,
mais pas encore intégré dans leur dernière release, vous pouvez demander à Composer de l'appliquer lors
de l'installation des dépendances (i.e. composer install
).
La lib composer-patches pour Composer, permet cette fonctionnalité.
Pour l'utiliser, ajoutez-la dans votre composer.json
. Par exemple :
{
// ...
"require": {
// ...
"cweagans/composer-patches": "~1.0",
"drupal/core": "~8.4.0"
},
// ...
"extra": {
// ...
"patches": {
"drupal/core": {
"Quickedit missing css classes": "https://www.drupal.org/files/issues/2551373-35.patch"
}
}
}
}
Explications :
composer-patches
comme dépendance.drupal/core
, que l'ont veut patcher.Remarque :
On peut appliquer plusieurs patchs successifs pour une ou plusieurs lib. Ex :
{
// ...
"require": {
// ...
"cweagans/composer-patches": "~1.0",
"vendor1/lib1": "~1.0",
"vendor1/lib2": "~1.0",
"vendor2/lib3": "~1.0"
},
// ...
"extra": {
// ...
"patches": {
"vendor1/lib1": {
"Patch 1": "https://www.vendor1.org/lib1/patch1.patch",
"Patch 2": "https://www.vendor1.org/lib1/patch2.patch"
},
"vendor2/lib3": {
"Patch 3": "https://www.vendor2.org/lib3/patch.patch"
}
}
}
}
Modifié le :
Publié le :
Les render array sont utilisés partout dans Drupal, pour générer des affichages. Lorsqu'on crée un bloc côté PHP, il retourne un render array. Même chose pour un formulaire ou pour un contrôleur.
Les modules peuvent définir leurs propres apparences pour les éléments : ils peuvent décrire de nouveaux render array (hook_theme()) qui seront ensuite utilisables partout.
Pour ajouter des variables à un render array proposé par un autre module, il faut tout d'abord modifier sa définition, pour que la variable puissent être transmise au template. On utilise pour cela le hook_theme_registry_alter().
/**
* Implements hook_theme_registry_alter().
*/
function mon_module_theme_registry_alter(&$theme_registry) {
$theme_registry['nom_du_render_array']['variables']['nouvelle_variable'] = 'valeur_par_defaut';
}
Maintenant que la variable "est autorisée", il faut l'ajouter au render array existant. Il s'agit d'un simple hook_preprocess_HOOK().
/**
* Implements hook_preprocess().
*/
function mon_module_preprocess_nom_du_render_array(&$variables) {
$variables['nouvelle_variable'] = 'valeur de la variable';
}
Explication :
nom_du_render_array
est la clé définie dans le hook_theme()
du module qui le propose.
C'est celle qu'on renseigne lorsqu'on utilise le render array ('#theme" => 'nom_du_render_array'
).hook_preprocess_HOOK()
.Remarque :
Comme après chaque implémentation de hook(), pensez à vider les caches.
Publié le :
Si vous avez créé un disque avec VirtualBox et qu'il est trop petit, vous pouvez l'agrandir.
Vous pouvez voir les disques d'une VM en cliquant-droit dessus : Configuration... > Stockage.
Avant toute chose, prenez le temps de créer un clone intégral de votre VM. En cas de problème vous n'aurez ainsi rien perdu.
Tout d'abord, il faut que votre disque soit au format vdi (et pas vmdk). Si ce n'est pas le cas, utilisez la commande suivante :
VBoxManage clonehd "source.vmdk" "cloned.vdi" --format vdi
Si votre disque a une taille fixe et non pas allouée dynamiquement, ajoutez l'option --variant Standard
à la commande précédente pour en changer (vous pourrez revenir à l'état précédent par la suite).
Il faut ensuite modifier la taille du disque, via la commande :
VBoxManage modifyhd "cloned.vdi" --resize 51200
51200
est la nouvelle taille en Mo.
Vous avez maintenant un disque au format vdi, avec une taille dynamique. Pour retrouver un disque au format vmdk, et/ou de taille fixe, inversez la première commande :
VBoxManage clonehd "cloned.vdi" "source.vmdk" --format vmdk --variant Fixed
Votre disque a maintenant la taille souhaitée, mais la partition n'a pas changé. Vous avez donc tout un espace disque non alloué.
Vous devriez maintenant avoir un disque dur plus grand et fonctionnel !
Modifié le :
Publié le :
Par défaut, les dépôts de Debian 8 proposent d'installer PHP 5.6. Si vous voulez la version 7, procédez ainsi.
apt
, en tant que super administrateur :echo 'deb http://packages.dotdeb.org jessie all' > /etc/apt/sources.list.d/dotdeb.list
wget -O- https://www.dotdeb.org/dotdeb.gpg | apt-key add -
apt update
gd
, mcrypt
, php-pear
, intl
:apt-get -y install php7.0 libapache2-mod-php7.0 php-pear php7.0-gd php7.0-mcrypt php7.0-intl
Globalement, les paquets portent le même nom que ceux pour PHP 5.6.
Modifié le :
Publié le :
Par défaut, les dépôts de Debian 8 propose d'installer PHP 5.6. Si vous voulez la version 7, procédez ainsi.
apt
, en tant que super administrateur :echo 'deb http://packages.dotdeb.org jessie all' > /etc/apt/sources.list.d/dotdeb.list
wget -O- https://www.dotdeb.org/dotdeb.gpg | apt-key add -
apt update
gd
, mcrypt
, php-pear
, intl
:apt-get -y install php7.0 libapache2-mod-php7.0 php-pear php7.0-gd php7.0-mcrypt php7.0-intl
Globalement, les paquets portent le même nom que ceux pour PHP 5.6.
Modifié le :
Publié le :
Pour installer la version de PHP disponible par défaut sur le dépôt (PHP 5.6 pour Debian 8), procédez comme suit. Pour installer PHP 7, suivez cet article à la place.
sudo apt-get install libapache2-mod-php5
gd
, mcrypt
, php-pear
, intl
:sudo apt-get install php5-gd php5-mcrypt php-pear php5-intl
Pour curl
, il s'agit du paquet php5-curl
.
/etc/php5/apache2/php.ini
.
À la fin de la section [Miscellaneaous]
, ajoutez la ligne suivante pour spécifier la locale à utiliser :date.timezone = "Europe/Paris"
sudo service apache2 restart
index.html
de votre site (ex : /var/www/mon-site/index.html) et
créez le fichier index.php
à la place :<?php
phpinfo();
?>
Si vous avez besoin de Composer, installez-le via ces commandes :
sudo apt-get install curl
curl -sS https://getcomposer.org/installer | php
sudo mv composer.phar /usr/local/bin/composer
# Vérification
composer --version