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
Modifié le :
Publié le :
Remarque :
Si vous préférez MySQL, vous pouvez suivre ce tutoriel à la place.
Il vous faudra peut-être également ajouter la ligne suivante au fichier /etc/apache2/apache2.conf
puis redémarrer Apache :
Include /etc/phpmyadmin/apache.conf
sudo apt-get install postgresql postgresql-client postgresql-doc
ls /etc/postgresql/
sudo pg_dropcluster --stop 9.4 main
sudo pg_createcluster --start -e UTF-8 9.4 main
Par défaut, PostgreSQL n'est pas accessible à distance (ex: avec votre IDE sur votre poste de dev).
Pour l'autoriser, modifiez le fichier le fichier de configuration /etc/postgresql/<version>/main/pg_hba.conf
,
en remplaçant la ligne suivante par celle du dessous :
#local all all peer
local all all trust
Redémarrez le service postgresql
:
sudo /etc/init.d/postgresql reload
Quand on utilise MySQL, on le couple souvent avec PHPMyAdmin, pour pouvoir l'administrer via une interface web. Côté PostgreSQL, il n'y a pas d'outil aussi abouti.
Adminer ne s'en tire tout de même pas si mal. D'ailleurs, il permet aussi de se connecter à plein d'autres SGBD, dont MySQL.
Pour l'installer, suivez les commandes suivantes :
sudo mkdir /var/www/adminer
sudo cd /var/www/adminer
sudo wget https://www.adminer.org/latest.php
sudo mv latest.php index.php
sudo chown -R phpuser:phpuser .
Ensuite si vous utilisez les virtual host pour votre site, ajoutez-en un pour adminer.
Pour cela créez par exemple le fichier /etc/sites-available/adminer.conf
:
<VirtualHost *:80>
ServerName db.adminer.dev
ServerAdmin webmaster@localhost
DocumentRoot /var/www/adminer
ErrorLog ${APACHE_LOG_DIR}/adminer.error.log
CustomLog ${APACHE_LOG_DIR}/adminer.access.log combined
</VirtualHost>
Remarques :
db.adminer.dev
.hosts
de votre poste de
dev pour pouvoir l'utiliser.Activez maintenant ce virtual host et redémarrez Apache :
sudo a2ensite adminer
sudo service apache2 reload
Par défaut, la base de données postgres
a été créée. Son propriétaire s'appelle aussi postgres
.
Un nouvel utilisateur système postgres
a lui aussi été créé.
sudo su - postgres
createuser --interactive mypguser
Avec les options suivantes :
Non
Oui
Non
Modifiez son mot de passe :
psql
> ALTER USER mypguser WITH PASSWORD 'new_password';
createdb -O mypguser mypgdatabase
psql mypgdatabase
> ALTER SCHEMA public OWNER TO mypguser;
sudo /etc/init.d/postgresql reload
Connectez-vous à la base avec le nouvel utilisateur :
psql -d mypgdatabase -U mypguser
Si vous n'avez pas d'erreur, alors c'est prêt (\q
pour quitter la console postgreSQL).
Modifié le :
Publié le :
Lorsque vous êtes sur la page qui liste les types de contenu par exemple, il y a le bouton Ajouter un type de contenu en haut à gauche.
Plus généralement, lorsqu'on liste des éléments (nœuds, liens de menu, ...), on propose souvent un lien pour en ajouter de nouveaux.
Si vous utilisez les vues Drupal pour ajouter des pages de liste en back-office,
vous vous voudrez probablement ce genre de bouton. Cela ne se fait pas dans la configuration de la vue,
mais via un fichier my_module.links.action.yml
. Par exemple :
# Article
node.add.article
route_name: node.add
route_parameters:
node_type: article
title: 'Ajouter un Temps Fort'
appears_on:
- view.my_view_article.display_1
- view.other_view.display_name
Explication :
On indique
Remarque :
Pour une vue, l'id de la route est composée du préfixe 'view', du nom technique de la vue, puis du nom technique de l'affichage de la vue (une vue pouvant avoir plusieurs affichages).
Ces deux ID techniques sont visibles dans l'URL lorsque vous éditez un affichage d'une vue. Ex : http://www.mysite.fr/admin/structure/views/view/`my_view_article`/edit/`display_1`.
Modifié le :
Publié le :
Lancez simplement la commande suivante :
sudo apt-get install apache2
sudo nano /etc/apache2/conf-available/charset.conf
AddDefaultCharset UTF-8
Pour éviter des problèmes de droits, vous pouvez modifier l'utilisateur et le groupe unix utilisés par Apache.
Par défaut avec Debian, il s'agit de www-data:www-data
.
Pour en changer, modifiez le fichier /etc/apache2/envvars
avec les droits administrateur :
export APACHE_RUN_USER=phpuser
export APACHE_RUN_GROUP=phpuser
Pour activer le module rewrite
d'Apache et redémarrez Apache, utilisez ces commandes :
sudo a2enmod rewrite
sudo service apache2 restart
cd /var/www
sudo mkdir mon-site
sudo chown -R phpuser:phpuser mon-site
index.html
à l'intérieur, contenant par exemple <h1>Hello world !</h1>
Si vous souhaitez accéder à votre site via un nom de domaine spécifique (plutôt que par son IP), ou si vous en avez besoin de plusieurs pour accéder à votre application, il vous faut configurer un virtual host.
Créez un nouveau fichier .conf
dans le répertoire des sites disponibles d'Apache
/etc/apache2/sites-available/
. (Ex: /etc/apache2/sites-available/mon-site.conf).
<VirtualHost *:80>
ServerAdmin admin@mon-site.com
ServerName www.mon-site.com
ServerAlias *.mon-site.com
DocumentRoot /var/www/mon-site/
<Directory /var/www/mon-site/>
Options FollowSymLinks
AllowOverride All
Require all granted
</Directory>
ErrorLog ${APACHE_LOG_DIR}/mon-site.log
ServerSignature Off
</VirtualHost>
Explications :
ServerAdmin
: Adresse email de la personne à prévenir en cas de problème côté Apache.ServerName
: Nom de domaine que vous souhaitez associer au serveur.
Il doit être dans les DNS du serveur (ou si vous êtes en mode dev, dans votre fichier /etc/hosts
).ServerAlias
: Autres domaines ou sous domaines qui prendront en compte le même fichier vHost.DocumentRoot
: Répertoire vers lequel Apache redirigera les adresses IP et ports spécifiés
plus haut (*:80).Directory
: Cette instruction permet d'ajouter des options et règles au répertoire web :
FollowSymLinks
: Active le suivi des liens symboliques dans ce répertoire.Activez votre virtual host, désactivez éventuellement celui par défaut puis redémarrez Apache :
sudo a2ensite mon-site
sudo a2dissite default
sudo service apache2 restart
Appelez la page d'accueil de votre site via un navigateur ou wget depuis votre répertoire utilisateur :
cd ~
wget mon-site.com
cat index.html
Vous devez retrouver le contenu de votre fichier index.html
:
<h1>Hello world !</h1>
Modifié le :
Publié le :
Cette suite d'articles donne un exemple d'installation d'un environnement LAPP sous Debian, destiné au développement (= pas pour la production). Il peut tout à fait servir de base pour un LAMP (= MySQL à la place de PostgreSQL)
Dans cet exemple les configurations choisies sont en italique.
root/******
apt-get install sudo
Ajoutez l'utilisateur phpuser
au groupe sudo :
adduser phpuser sudo
Par défaut, Debian 8 n'autorise pas cette connexion, ce qui est gênant lors de l'utilisation d'un outil comme WinSCP, Filezilla ou Nautilus pour explorer les fichiers.
Pour l'autoriser, modifiez le fichier /etc/ssh/sshd_config
et remplacer la ligne :
PermitRootLogin without-password
par
PermitRootLogin yes
Redémarrez ensuite le service SSH :
service ssh restart
Modifier le fichier .bashrc
permet d'améliorer l'affichage de la console et
d'ajouter des alias de commande.
.bashrc
avec nano et décommentez les lignes suivantes :alias ll='ls -l'
alias la='ls -1'
alias l='ls -CF'
Si vous voulez avoir une console colorée, décommentez la ligne suivante :
force_color_prompt=yes
et modifiez la ligne suivante en la remplaçant par celle du dessous :
#PS1='${debian_chroot:+($debian_chroot)}\u@\h:\w\$ '
PS1='\[\e[33;01m\]\u \[\033[31;01m\][\[\033[0;37m\]\w\[\033[31;01m\]] \$\[\033[00m\] '
export PATH=$PATH:/usr/sbin
sudo apt-get update
sudo apt-get upgrade
sudo apt-get dist-upgrade
/etc/apt/sources.list
avec nano en tant qu'administrateur :deb http://security.debian.org/ etch/updates main contrib non-free
sudo apt-get update
sudo apt-get install build-essential
Installez les outils dont vous avez besoin (ex: Screen, ...).
Modifié le :
Publié le :
Le module Help fait partie du cœur de Drupal et est activé par défaut.
Il permet d'ajouter un message au haut de la page d'édition d'un contenu :
Pour cela il suffit d'éditer votre type de contenu, et de remplir la zone de texte sous le champ title (le HTML est accepté) :
Pour que le message apparaisse dans le formulaire d'édition, il faudra que le bloc Aide soit activé
dans une des régions de votre thème d'administration. C'est le cas pour celui par défaut (Seven
).
Modifié le :
Publié le :
Pour ajouter une classe CSS sur un élément de formulaire basique (input, select, ...),
on ajoute la clé #attributes
à son render array.
Par contre pas possible de le faire pour une liste de boutons radio par exemple. La classe s'ajoutera sur le conteneur à la place.
Il existe donc la clé #after_build
pour remédier à ce problème (cf. documentation).
Elle attend une liste de noms de fonction en valeur.
Chacune de ces fonctions sera exécutée après coup pour modifier le render array de l'élément. À ce moment de l'exécution, les sous-éléments (ici les boutons radio) ont déjà été ajoutés et peuvent donc être modifiés.
Par exemple dans ma méthode buildForm()
:
$form['my_field'] = [
'#type' => 'radios',
'#title' => t('My field'),
'#options' => [
0 => t('No'),
1 => t('Yes'),
],
'#after_build' => ['_my_module_radio_add_class']
];
Et dans mon fichier my_module.module
:
function _my_module_radio_add_class(array $element, FormState $form_state) {
$options = array_keys($element['#options']);
// Parcours des sous-éléments options
foreach ($options as $values) {
$element[$values]['#attributes']['class'][] = 'myclass';
}
return $element;
}
Modifié le :
Publié le :
Si vous voulez rediriger l'utilisateur vers la page 404 ("Page introuvable") ou 403 ("Vous n'avez pas le droit d’accéder à cette page") de Drupal, vous utilisiez sans doute ça en Drupal 7 :
return drupal_access_denied();
return drupal_not_found();
Voici l'équivalent pour Drupal 8 :
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
throw new AccessDeniedHttpException();
throw new NotFoundHttpException();
Modifié le :
Publié le :
Si vous utilisez Drupal dans un sous-répertoire servi par Apache (ex : /var/www/html/mon_site
),
ou derrière un reverse proxy qui ajoute un contexte à l'URL (ex : http://mon-domaine.fr/mon-contexte
),
il est possible que seule la page d'accueil fonctionne.
Vous aurez alors une erreur 404 sur toutes les autres pages (ex : http://mon-domaine.fr/mon-contexte/user/login
).
Il vous faut alors modifier le .htaccess
à la racine du site, en modifiant la ligne :
# RewriteBase /
Décommentez-la et remplacer le /
par le contexte ou le sous-répertoire. Ex :
# Modify the RewriteBase if you are using Drupal in a subdirectory or in a
# VirtualDocumentRoot and the rewrite rules are not working properly.
# For example if your site is at http://example.com/drupal uncomment and
# modify the following line:
# RewriteBase /drupal
#
# If your site is running in a VirtualDocumentRoot at http://example.com/,
# uncomment the following line:
RewriteBase /mon-contexte
Publié le :
Pour connaître le poids total des fichiers (et sous-répertoires) que contient un répertoire,
vous pouvez utiliser la commande du
:
du -shL mon/repertoire
Explication :
Les options s
, h
et L
permettent respectivement de
faire la somme, afficher un poids lisible par un humain (en Ko, Mo, ...)
plutôt qu'en octets et suivre les liens symboliques (plutôt que de compter le poids du lien).
Pour connaître le nombre de fichiers il faut cette fois composer avec plusieurs commandes :
find -L mon/repertoire -type f | wc -l
Explications :
find
avec l'option -type f
permet de lister les fichiers-L
permet de suivre les liens symboliqueswc -l
compte le nombre de lignes (ici celles retournées par find
)Modifié le :
Publié le :
Si vous installez un module dans Drupal et que vous en supprimez les fichiers avant de le désinstaller, vous pouvez rencontrer une erreur du genre :
The following module is missing from the file system:
paragraphs in drupal_get_filename() (line 240 of core/includes/bootstrap.inc).
Pour Drupal, le module est désinstallé. Pourtant, il en garde des traces et cela cause cette erreur.
Deux solutions sont alors possibles :
drush sql-query "DELETE FROM key_value WHERE collection='system.schema' AND name='module_name';"
Modifié le :
Publié le :
Lorsque vous mettez à jour vos dépendances avec Composer, vous ne souhaitez pas avoir la dernière version disponible.
Par exemple, si j'ai un site Drupal en v8.2.7 et qu'une mise à jour de sécurité sort, je peux préférer passer en v8.2.8 plutôt qu'en 8.3.1.
Dans ce cas on peut préciser ça dans le fichier composer.json
:
{
...
"require": {
"drupal/core": "~8.2.0",
...
},
...
}
Explications :
^
, moins restrictif : ^8.2.1 signifie >=8.2.1 & < 9Modifié le :
Publié le :
PhpStorm et IntelliJ proposent d'ajouter un fichier au versionnement (ex: git add), mais ne permettent pas de le retirer.
Une solution de contournement simple consiste à le faire en ligne de commande :
git rm --cached mon_fichier
git commit -m 'suppression du fichier'
pour un répertoire entier, ajoutez l'option -r
écursive :
git rm -r --cached mon_repertoire
git commit -m 'suppression du répertoire'
Modifié le :
Publié le :
Si vous êtes sous une distribution avec apt d'installé, vous pouvez rechercher un paquet avec la commande suivante :
apt-cache search terme1 terme2
Remarques :
apt-cache search php.*memcach
.Si vous avez aptitude d'installé, une commande plus simple à mémoriser existe :
aptitude search terme1 terme2
Modifié le :
Publié le :
Si vous utilisez une VM LAMP et PHPStorm, vous pouvez utiliser son débogueur pas à pas et visualiser/modifier vos variables en temps réel au cours de l'exécution.
Pour cela, plusieurs étapes sont nécessaires.
Connectez-vous à votre VM, et lancez la commande d'installation :
sudo apt-get install php5-xdebug
Activez ensuite le débogage distant de xdebug en éditant le fichier /etc/php5/apache2/conf.d/20-xdebug.ini
.
Ajoutez-y les lignes suivantes :
xdebug.remote_enable=1
xdebug.remote_host=192.168.0.1
xdebug.remote_port=9000
xdebug.max_nesting_level=500
Remarques :
Remplacez l'IP par celle de la machine sur laquelle vous lancez PHPStorm. La dernière ligne évite une erreur de récursion lors du débogage
Ajoutez une nouvelle connexion SSH à votre VM :
+
vert à droite, pour ajouter un environnement/var/www/mon-site
), ou un répertoire parent (ex : /var/www
)/
ou /mon-site
)/
si vous utilisez un nom de domaine pointant directement vers votre projetPrécisez le mapping :
/var/www/monsite/web
ou /var/www/monsite/vendor
)Pour déclencher le débogage, il faut ajouter un cookie à vos requêtes lors de votre navigation.
L'extension easy Xdebug pour Firefox et XDebug Helper pour Chrome permettent d'activer/désactiver ce cookie automatiquement via un bouton.
Le site officiel pour PHPStorm liste d'autres extensions pour d'autres navigateurs.
Activez le débogage dans votre navigateur.
Cliquez sur le bouton Start Listening for PHP Debug Connections présent dans la barre d'outil de PHPStorm (téléphone gris avec un sens interdit rouge et un insecte vert).
Affichez une page de votre site.
Lors du premier débogage, une popup apparaît dans PHPStorm vous demandant de confirmer le débogage (normalement la configuration présentée dans la popup n'a pas besoin d'être modifiée).
Pour les suivants, vous devriez voir apparaître une petite notification dans PHPStorm vous disant que le débogage a fonctionné, mais ne s'est pas arrêté. Dans cette notification, vous pouvez activer l’arrêt automatique à la première ligne du fichier index.php. Sinon, ajoutez un point d'arrêt (clic dans la marge de gauche du fichier PHP à déboguer). Rechargez la page.
Modifié le :
Publié le :
Si vous utilisez Composer dans une VM, vous pouvez avoir une erreur de mémoire dûe à un problème de swap :
Installation failed, reverting ./composer.json to its original content.
The following exception is caused by a lack of memory or swap, or not having swap configured
Check https://getcomposer.org/doc/articles/troubleshooting.md#proc-open-fork-failed-errors for details
[ErrorException]
proc_open(): fork failed - Cannot allocate memory
Plusieurs solutions sont décrites dans ce ticket sur Stackoverflow.
Celle (temporaire) qui marche particulièrement bien est la suivante. Il suffit de lancer les commandes :
/bin/dd if=/dev/zero of=/var/swap.1 bs=1M count=1024
/sbin/mkswap /var/swap.1
/sbin/swapon /var/swap.1
Modifié le :
Publié le :
Pour créer un lien symbolique sou linux, il suffit d'utiliser la commande suivante :
ln -s chemin/vers/la/cible/du/lien chemin/vers/lien
Le premier paramètre contient le répertoire ou fichier à cibler, et le second le chemin/nom du lien à créer.
Les chemins peuvent-être relatifs ou absolus.
Modifié le :
Publié le :
Sur les dépôts des distributions linux, c'est souvent une vieille version de drush qui est disponible (ex: Debian 8.4 -> drush 5.x). Voici comment installer la dernière.
Composer et Git doivent être installés.
sudo apt-get install curl
sudo curl -sS https://getcomposer.org/installer | php
sudo mv composer.phar /usr/local/bin/composer
# Vérification
composer --version
sudo apt-get install git
# Vérification
git --version
sudo git clone --depth 1 https://github.com/drush-ops/drush.git /usr/local/src/drush
cd /usr/local/src/drush
sudo composer install
sudo ln -s /usr/local/src/drush/drush /usr/local/bin/drush
sudo ln -s /usr/local/src/drush/drush.complete.sh /etc/bash_completion.d/drush
drush --version
Modifié le :
Publié le :
Si vous utilisez Virtual Box sous Windows pour émuler des VM sous Linux, avec des répertoires partagés, vous aurez surement des problèmes de création de liens symboliques dans ces répertoires.
Il suffit de créer les liens symboliques côté Windows et non pas côté VM. Cependant, cela peut ne pas être suffisant, si vous utilisez une version VirtualBox 4.1.8 ou ultérieure.
Dans ce cas, il vous faut modifier un paramètre désactivé par défaut, via la commande suivante :
"C:\Program Files\Oracle\VirtualBox\VBoxManage.exe" setextradata "<nom_de_la_vm>" VBoxInternal2/SharedFoldersEnableSymlinksCreate/<nom_du_partage> 1
Modifié le :
Publié le :
Drupal 8 propose nativement des pages pour gérer l'inscription, la connexion, l'oubli de mot passe.
Malheureusement actuellement il n'y a pas de suggestion de template proposée. (Comme on peut le voir habituellement en commentaire dans le code source lorsque le mode debug est activé.)
Il faut donc procéder autrement et utiliser les hook_form_alter() et hook_theme() classiques.
Par exemple, pour surcharger le formulaire de la page oubli de mot de passe :
// mymodule.module
/**
* Implements hook_form_alter()
*/
function mymodule_form_alter(&$form, \Drupal\Core\Form\FormStateInterface $form_state, $form_id) {
// Si le formulaire est celui d'oubli de mot de passe
if ($form_id == 'user_pass') {
$form['#theme'] = ['my_register_form'];
}
}
/**
* Implements hook_themer()
*/
function mymodule_theme(&$form, \Drupal\Core\Form\FormStateInterface $form_state, $form_id) {
return [
'my_password_form' => [
'template' => 'user_forget_password_form',
'render element' => 'form',
],
];
}
Explications :
hook_form_alter()
permet de modifier le thème à utiliser pour le formulaire.
Le thème choisi doit exister ou être déclaré dans votre module.hook_theme()
permet de déclarer le nouveau thème my_password_form
et d'y affecter
un template spécifique.Remarque :
Par défaut, sans cette configuration, le template natif form.html.twig
serait utilisé.
Pour créer votre propre template il peut donc être pratique d'en faire une copie,
la renommer (ici user_forget_password_form.html.twig
) et de s'en servir comme base pour effectuer
vos modifications.
Modifié le :
Publié le :
Pour éviter que ses variables et fonctions ne soient disponibles partout et ne rentrent en conflit entre-elles, il est important d'isoler son code.
Pour cela, plusieurs solutions sont possibles, en fonction des cas.
Cela consiste à englober son code pour que les variables et fonctions deviennent locales.
(function($) {
// Code à exécuter
// [...]
})(jQuery);
Cela consiste à placer ses variables et fonctions dans un namespace autre que celui par défaut (= lorsqu'on n'en précise pas).
$.myNamespace = {
multiply: function(x,y) {
return (x * y);
}
};
// Appel
var calc = $.myNamespace.multiply(1, 1);
On utilise le namespace de jQuery : $
ou jQuery
.
$.fn.myFunction = function(params) {
this.css('color', 'red');
return this;
};
// Appel
$('.element').myFonction();
Explication :
On crée un plugin jQuery, que l'on appelle sur un ensemble d'éléments
du DOM (ex: $('.element')
). Tous ces éléments sont impactés une fois, sans mémorisation de leur état (= stateless).
$.widget('ui.trsb.truc.myWidget', [$.ui.widgetParent,]{
"options": {},
myWidgetFunction(){...},
_create: function(){...},
_destroy: function(){...},
_setOptions: function(key, value){...},
});
// Appel
$('.element').myWidget();
$('.element').myWidgetFunction();
Explications :
On crée un widget jQuery (nécessite jQueryUI), que l'on appelle sur un ensemble d'éléments
du DOM (ex: $('.element')
). Tous ces éléments sont impactés avec mémorisation de leur état (= statefull).
Les widgets sont utiles lorsque l'on veut proposer plusieurs méthodes d'action (ex: un accordéon avec une fonction pour dérouler, et une autre pour replier).
// Pour des raisons de compatibilité, préférez
$('#container').find('.children').hide();
// à
$('#container .children').hide();
// ou à
$('.children', '#container').hide();
$('#link').on('click', function() {
// Préférez
this.id
// à
$this.attr(id');
});
// Préférez
$(function() {
});
// à
$(document).ready(function() {
});
// Préférez
$('#element').on('click', function() {
});
// à
$('#element').click(function() {
});
// Préférez
(function($) {
// [...] Code sans conflit
})(jQuery);
// à
jQuery.noConflict();
// [...] Code sans conflit
// Préférez
$.ajax()
// à
$.getJSON();
Modifié le :
Publié le :
Les commandes drush pour Drupal 8 sont en partie identiques à celles pour Drupal 7. En fait, il s'agit surtout de la version de drush et non pas de celle de Drupal. Pour Drupal 8, il est conseillé d'utiliser la version 8.x de drush.
Voici une liste de commandes drush bien pratiques :
Fonction | Commande |
---|---|
Exporter un nouveau composant dans une feature | drush cex |
Importer les configurations du site | drush cim |
Fonction | Commande |
---|---|
Information sur un module | drush pmi nom_module |
Télécharger un module | drush dl nom_module |
Activer un module | drush en nom_module |
Désinstaller un module | drush pmu nom_module |
Mettre à jour les tables en base concernant un module | drush updb nom_module |
Liste des modules communautaires activés | drush pm-list --pipe --type=module --status=enabled --no-core |
Liste des modules du cœur activés | drush pm-list --pipe --type=module --status=enabled --core |
Fonction | Commande |
---|---|
Exécuter une commande SQL | drush sqlc "SELECT * FROM node;" |
Créer un dump | drush sql-dump > /chemin/fichier.sql |
Vider une base de données | drush sql-drop |
Importer un dump | drush sql-cli < /chemin/fichier.sql |
Mettre à jour les tables en base pour tous les modules | drush updb (utile après une mise à jour de sécurité) |
Mettre à jour entités en base | drush entup (utile après une mise à jour de module) |
Fonction | Commande |
---|---|
Vider tous les caches | drush cr |
Modifier le mot de passe d'un utilisateur | drush upwd --password="nouveau_mot_de_passe" login_utilisateur |
Exécuter une tâche planifiée | drush php-eval 'monmodule_cron();' |
Exécuter du code PHP | drush php-eval 'echo "je suis du code php exécuté";' |
Connaître la version de Drupal | drush status |
Modifié le :
Publié le :
Pour rendre votre site plus facilement paramétrable, il est utile de fournir une interface d'administration pour modifier telle ou telle option.
Ces options seront ensuite accessibles partout dans votre code :
$config = \Drupal::config('mon_module.settings');
$my_option_value = $config->get('my_option');
Comme son prédécesseur, Drupal 8 permet de générer rapidement ces interfaces, ainsi que les éléments du menu d'administration correspondant :
Pour générer deux pages de formulaires avec des onglets pour passer de l'un à l'autre, vous aurez besoin des fichiers suivants :
Remarque :
L'exemple qui suit requiert d'activer ces deux modules : admin_toolbar et admin_toolbar_tools.
Voici un exemple de formulaire d'administration affichant trois champs, de types numérique, texte riche et email.
<?php
namespace Drupal\my_module\Form;
use Drupal\Core\Form\ConfigFormBase;
use Drupal\Core\Form\FormStateInterface;
/**
* Gère le formulaire de configuration générale pour le module.
*/
class GlobalSettingsForm extends ConfigFormBase {
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'my_module_settings_form';
}
/**
* {@inheritdoc}
*/
protected function getEditableConfigNames() {
return [
'my_module.settings',
];
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
}
}
Explications :
ConfigFormBase
, fournie par Drupal.getFormId()
, buildForm()
et submitForm()
habituelles,
elle impose que la méthode getEditableConfigNames()
soit implémentée./**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
// Récupération de la configuration avec le "namespace" my_module.settings
$config = $this->config('my_module.settings');
$form['nb_news_homepage'] = array(
'#type' => 'number',
'#title' => $this->t('Nombre d\'actualités afichées en page d\'accueil'),
'#default_value' => $config->get('nb_news_homepage'),
);
$form['welcome_text'] = array(
'#type' => 'text_format',
'#title' => $this->t('Texte de bienvenue'),
'#description' => $this->t('Texte affiché en page d\'accueil.'),
'#default_value' => $config->get('welcome_text'),
);
$form['contact_receiver_email'] = array(
'#type' => 'email',
'#title' => $this->t('Adresse email du destinataire pour le formulaire de contact'),
'#default_value' => $config->get('contact_receiver_email'),
);
return parent::buildForm($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$this->config('my_module.settings')
->set('nb_news_homepage', $form_state->getValue('nb_news_homepage', 5))
->set('welcome_text', $form_state->getValue('welcome_text', '<p>Texte de bienvenue à changer.</p>')['value'])
->set('contact_receiver_email', $form_state->getValue('contact_receiver_email', 'admin@monsite.com'))
->save();
parent::submitForm($form, $form_state);
}
Explications :
La méthode buildForm()
est semblable à celle d'un formulaire classique. À noter cependant :
config()
, avec le "namespace" définit précédemmentLa méthode submitForm()
va enregistrer les données soumises en base :
Remarque :
Pour un champ texte riche, la méthode getValue()
proposée par FormStateInterface
retourne un
tableau et non pas une valeur enregistrable en base.
Il faut penser à ajouter ['value']
derrière, pour avoir une chaîne de caractère utilisable.
C'est le fichier classique de Drupal, permettant de lier des URL à vos contrôleurs et formulaires.
# Page générale listant les pages de configuration du module
my_module.overview:
path: '/admin/config/my_module'
defaults:
_controller: '\Drupal\system\Controller\SystemController::systemAdminMenuBlockPage'
_title: 'Mon module - Configuration'
link_id: 'my_module.overview'
requirements:
_permission: 'administer site configuration'
# Page de configuration générale du module
my_module.settings:
path: '/admin/config/my_module/general'
defaults:
_form: '\Drupal\my_module\Form\GlobalSettingsForm'
_title: 'Mon module - Configuration générale'
requirements:
_permission: 'administer site configuration'
# Page de configuration des webservices du module
my_module.webservices.settings:
path: '/admin/config/my_module/webservices'
defaults:
_form: '\Drupal\my_module\Form\WebservicesSettingsForm'
_title: 'Mon module - Configuration des webservices'
requirements:
_permission: 'administer site configuration'
Explications :
http://www.monsite.com/admin/config/people
)Ce fichier définit de nouveaux éléments dans le menu d’administration.
# Page générale listant les pages de configuration du module
my_module.overview:
title: 'Mon module'
parent: system.admin_config
description: 'Voir les pages de configuration du module "Mon module".'
route_name: my_module.overview
weight: -100
# Page de configuration générale du module
my_module.settings:
title: 'Général'
parent: my_module.overview
description: 'Gérer la configuration générale du module.'
route_name: my_module.settings
weight: -10
# Page de configuration des webservices du module
my_module.webservice.settings:
title: 'Webservices'
parent: my_module.overview
description: 'Gérer la configuration des webservices.'
route_name: my_module.webservices.settings
weight: -5
Explication :
Pour chaque lien souhaité, on définit :
title
pour le lien généré)Dans cet exemple, la page "overview" est parente des deux autres.
Ce fichier définit des onglets accessibles dans les pages d'administration. Depuis la première page, on a donc un lien rapide vers la seconde, et vice-versa.
my_module.settings:
route_name: my_module.settings
title: 'Général'
base_route: my_module.settings
my_module.webservices.settings:
route_name: my_module.webservices.settings
title: 'Webservices'
base_route: my_module.settings
Explication :
Pour chaque onglet souhaité, on définit :
Modifié le :
Publié le :
Dans Drupal 7, pour ajouter un nouveau mode d'affichage il fallait utiliser un hook (cf: cet autre article).
Dans Drupal 8.x, tout est faisable en back-office, via Structure > Modes d'affichage. Ensuite, comme avant, il faut activer le nouveau mode pour le type de nœud correspondant :
Remarque :
Dans Drupal 8, les modes d'affichage sont cloisonnés par entité. Si vous voulez un mode d'affichage Liste pour les utilisateurs et pour les nœuds, il faudra en créer deux.
Modifié le :
Publié le :
La plupart du temps dans Drupal, on définit la manière d'afficher des éléments via un tableau de theming côté PHP.
Par exemple, pour un bloc, on peut avoir un tableau du genre :
$build = array(
'#cache' => array(
'contexts' => array('user'),
'max-age' => Cache::PERMANENT,
),
'#markup' => '<p>Hello ' . $who . '</p>',
);
Dans cet exemple, on ne précise pas l'habillage à utiliser.
Drupal sélectionnera donc un template par défaut en fonction du type de l'élément
(block.html.twig
dans le cas d'un bloc).
Pour utiliser votre propre template, il faut modifier le tableau et remplacer #markup
par #theme
:
$build = array(
'#cache' => array(
'contexts' => array('user'),
'max-age' => Cache::PERMANENT,
),
'#theme' => 'my_hello',
'#who' => $who,
);
Pour que Drupal trouve votre habillage, vous devez implémenter le hook_theme() dans votre module.
// my_module.module
/**
* Implements hook_theme().
*/
function my_module_theme() {
return [
'my_hello' => [
'template' => 'my_hello_box',
'variables' => [
'who' => 'World',
],
]
];
}
Explications :
my_hello
.my_hello_box.html.twig
.who
sera transmise au template, avec la valeur 'World'
par défaut.Le template my_hello_box.html.twig
placé dans le répertoire templates/
de votre module
peut ressembler à ça :
{# Affichage d'un message Hello sous forme de boîte #}
<div class="box">
<p>{{ 'Hello %who !'|t({ 'who': who }) }}</p>
</div>
Remarque :
Vous pouvez placer votre template dans n'importe quel sous répertoire de templates/
.
Drupal saura le trouver.
Modifié le :
Publié le :
Pour créer un bloc programmatiquement, vous devez déjà avoir créé un module.
Dans cet exemple, on créera un bloc qui affiche "Hello" et le nom de l'utilisateur connecté. On pourra personnaliser qui saluer si aucun utilisateur n'est connecté.
Toute la déclaration/configuration du bloc se fait dans une classe PHP,
placée traditionnellement dans le répertoire src/Plugin/Block/
de votre module :
<?php
namespace Drupal\my_module\Plugin\Block;
use Drupal\Core\Block\BlockBase;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Form\FormStateInterface;
/**
* @Block(
* id = "hello_block",
* admin_label = @Translation("My Hello block"),
* category = @Translation("My project")
* )
*/
class HelloBlock extends BlockBase {
/**
* {@inheritdoc}
*/
public function build() {
$current_user = \Drupal::currentUser();
if (!$current_user->isAnonymous()) {
$who = $current_user->getDisplayName();
}
else {
$config = $this->getConfiguration();
$who = isset($config['who']) ? $config['who'] : 'World';
}
$build = array(
'#cache' => array(
'contexts' => array('user'),
'max-age' => Cache::PERMANENT,
),
'#markup' => '<p>Hello ' . $who . '</p>',
);
return $build;
}
/**
* {@inheritdoc}
*/
public function blockForm($form, FormStateInterface $form_state) {
$form = parent::blockForm($form, $form_state);
$config = $this->getConfiguration();
$form['my_block_who'] = array(
'#type' => 'textfield',
'#title' => $this->t('Who ?'),
'#default_value' => isset($config['my_block_who']) ? $config['my_block_who'] : 'world',
);
return $form;
}
/**
* {@inheritdoc}
*/
public function blockSubmit($form, FormStateInterface $form_state) {
$this->configuration['my_module_who'] = $form_state->getValue('my_module_who');
}
}
Explications :
BlockBase
, fournie par Drupal.build()
, qui retourne un tableau de theming Drupal pouvant être rendu à l'écran.Remarque :
Le tableau de theming définit un cache par utilisateur (#cache
), et le code HTML (#markup
)
constituant le contenu de votre bloc.
Pour que Drupal trouve votre nouveau bloc, vous devez vider les caches.
Apparaît alors le formulaire d'administration du bloc.
Vous pouvez par défaut choisir pour quelles pages le module doit apparaître (en fonction de l'utilisateur, du contenu affiché en pleine page, de l'URL de la page, ...).
À ce paramétrage s'ajoute votre champ personnalisé Who ?.
Par défaut, le bloc est affiché en suivant le template block.html.twig
fourni par Drupal et
le thème que vous utilisez.
Pour surcharger ce template, vous pouvez définir un nouvel habillage pour votre bloc et créer votre propre template twig.
Modifié le :
Publié le :
La création d'un module est très rapide dans Drupal. Dans cet exemple, on créera le module mymodule
.
Commencez par créer le répertoire mymodule
. Vous pouvez le placer directement dans modules,
ou créer un répertoire intermédiaire qui regroupera tous vos modules
(ex: modules/myproject/mymodule
ou modules/custom/mymodule
).
Basiquement, un module n'a besoin que de deux fichiers, tous deux à la racine du répertoire :
mymodule.info.yml
et mymodule.module
.
Le fichier .info.yml
permet de décrire votre module.
name: Mon module
description: Module d'exemple.
package: Mon projet
type: module
version: '8.x-1.x'
core: '8.x'
project: 'mon_module'
Explications :
Le fichier .module
contiendra du code PHP. Pour l'instant, créez un
fichier mymodule.module
vide (avec uniquement <?php
et au moins un retour à la ligne).
Votre module contiendra probablement par la suite deux répertoires principaux :
Une fois fait, vous devriez voir votre module en back-office :
Vous pouvez l'activer via cette interface ou utiliser drush :
drush en mymodule -y
Modifié le :
Publié le :
Pour créer un nouveau contenu (ou n'importe quelle entité), quelques lignes suffisent :
<?php
use Drupal\node\Entity\Node;
use Drupal\Core\Entity\EntityStorageException;
$node = Node::create([
'type' => 'article',
'langcode' => 'fr',
'uid' => '1',
'status' => 1,
]);
$node->setTitle('Mon premier article');
$node->set('field_my_text', 'du texte');
$node->set('field_my_float', 150.42);
$node->set('field_my_date', date('Y-m-d'));
try {
$node->save();
}
catch (EntityStorageException $e) {
\Drupal::logger('mymodule')->error("La création de l'article a échouée : @message", [
'@message' => $e->getMessage()
]);
$node = NULL;
}
Explications :
create()
permettent de définir le type de nœud,
son créateur, son statut, sa langue, ...set()
.Modifié le :
Publié le :
Après l'installation d'un site chez un hébergeur, un problème peut survenir lors de l'import/du téléchargement des fichiers de traduction de l'interface :
Warning: move_uploaded_file(translations://fr.po): failed to open stream: "Drupal\locale\StreamWrapper\TranslationsStream::stream_open" call failed in Drupal\Core\File\FileSystem->moveUploadedFile() (line 79 of core/lib/Drupal/Core/File/FileSystem.php).
Drupal\Core\File\FileSystem->moveUploadedFile('/tmp/phpxTna7m', 'translations://fr.po') (Line: 856)[...]
La solution consiste à vérifier les droits sur le répertoire sites/default/files/translations
.
S'il n'existe pas, créez-le et donner le droit d'écriture pour l'utilisateur apache.
Modifié le :
Publié le :
Le site https://regex101.com propose un outil complet de test d'expressions régulières :
Modifié le :
Publié le :
Il est parfois utile de choisir avec quel utilisateur système Apache est exécuté.
Pour le modifier, éditez le fichier /etc/apache2/envvars
et modifiez les lignes suivantes :
export APACHE_RUN_USER=www-data
export APACHE_RUN_GROUP=www-data
www-data
est la valeur par défaut. Il suffit de mettre l'utilisateur et le groupe que vous souhaitez.
Modifié le :
Publié le :
Avec Virtualbox, vous déclarez parfois deux interfaces réseaux pour votre VM (ex: connexion par pont + réseau privé hôte).
Souvent, la deuxième interface n'est pas reconnue au démarrage de la VM.
Pour éviter cela, modifiez le fichier /etc/network/interfaces
, en ajoutant ces lignes, et redémarrez la VM :
allow-hotplug eth1
iface eth1 inet dhcp
Modifié le :
Publié le :
Avec Virtualbox, on peut partager des répertoires depuis son hôte vers la machine virtuelle.
Par défaut, Virtualbox les monte dans /media/sf_<nom_partage>
et attribut le répertoire à l'utilisateur vboxsf
.
Pour monter automatiquement le répertoire à l'endroit de votre choix, vous pouvez modifier le fichier /etc/rc.local
,
et ajouter cette ligne :
mount -o uid=500,gid=500,umask=002 --bind /media/sf_<nom_partage> /mon/repertoire/cible
Remarque :
Les options uid et gid permettent d'attribuer le répertoire à un utilisateur et à un groupe spécifique.
500
correspond par exemple à l'utilisateur d'Apache www-data
(la plupart du temps).
Modifié le :
Publié le :
Avec Virtualbox, on peut partager des répertoires depuis son hôte vers la machine virtuelle.
Par défaut, Virtualbox les monte dans /media/sf_<nom_partage>
et attribut le répertoire à l'utilisateur vboxsf
.
Pour monter automatiquement le répertoire à l'endroit de votre choix, vous pouvez modifiez le
fichier /etc/rc.local
, et ajouter cette ligne :
mount -o uid=500,gid=500,umask=002 --bind /media/sf_<nom_partage> /mon/repertoire/cible
Remarque :
Les options uid
et gid
permettent d'attribuer le répertoire à un utilisateur et à un groupe spécifique.
500
correspond par exemple à l'utilisateur d'Apache www-data
(la plupart du temps).
Modifié le :
Publié le :
Il arrive que Drupal génère des thumbnails avec des droits incorrects. Apache ne peut alors pas les servir et ils ne s'affichent pas dans le site.
C'est probablement parce que les droits sur le répertoire sites/default/files/
sont mauvais.
Exécutez ces commandes pour corriger ce problème :
find sites/default/files -type d -exec chmod 755 {} +
find sites/default/files -type f -exec chmod 644 {} +
chown -R www-data:www-data sites/default/files
Plus d'informations ici :
Modifié le :
Publié le :
Sous linux, vous pouvez rapidement connaître l'espace disque restant grâce à la commande :
df -h
Note : comme pour beaucoup d'autres commandes Linux, l'option -h
permet un affichage plus humain, notamment
pour le poids des fichiers (ie. 1.2mo au lieu de 1200000).
Modifié le :
Publié le :
Avec Symfony, on utilise très souvent le conteneur de services. Par exemple dans un contrôleur, si on veut récupérer le logger de Monolog :
$logger = $this->get('logger');
Pour connaitre la liste de tous les services disponibles, utilisez la commande suivante :
php app/console container:debug
Modifié le :
Publié le :
Si jamais vous avez perdu le mot de passe root de MySQL, il est possible de le réinitialiser, ou d'exécuter n'importe quelle commande SQL nécessitant normalement des droits administrateur.
Pour cela :
Rendez-vous dans le répertoire MySQL (par exemple D:\Dev\wamp\bin\mysql\mysql5.5.24
)
Créez un fichier SQL contenant les requêtes à effectuer. Par exemple pour réinitialiser le mot de passe root :
UPDATE mysql.user SET Password = PASSWORD ('nouveau_mot_de_passe') WHERE User = 'root';
FLUSH PRIVILEGES;
Arrêtez le service MySQL s'il est lancé
Ouvrez une invite de commande et déplacez-vous dans le répertoire MySQL
Lancez la commande suivante :
bin\mysqld.exe --defaults-file="D:\Dev\wamp\bin\mysql\mysql5.5.24\my.ini" --init-file="D:Dev\wamp\bin\mysql\mysql5.5.24\my_sql_script.sql" --console
Explications :
mysqld
doit recevoir 2 paramètres valués :
Modifié le :
Publié le :
Lorsque vous lancez les services de WampServer, il arrive qu'Apache n'arrive pas à démarrer à cause d'une erreur de syntaxe dans sa configuration.
Pour savoir quel fichier (httpd.conf
, httpd-vhosts.conf
, ...) et quelle ligne pose problème,
vous pouvez lancer Apache en ligne de commande :
cd D:\Dev\wamp\bin\apache\apache2.2.22\bin
httpd
Remarque : Adaptez le chemin en fonction d'où est installé WampServer et la version d'Apache que vous utilisez.
Exemple :
Modifié le :
Publié le :
Le fonctionnement d'un webservice REST est très proche de celui d'une page classique. On souhaite accéder à une ressource, la modifier, la supprimer, ...
On y accède via une requête HTTP, à laquelle on fournit des paramètres et/ou des données.
Voici les étapes à suivre pour créer un webservice REST dans Drupal, communicant en JSON.
Pour que Drupal gère l'URL du webservice, il faut implémenter le hook_menu()
habituel :
// my_module.module
/**
* Implements hook_menu().
*/
function my_module_menu()
{
$items = array();
// Webservice de lecture d'un article
$items['api/article/%'] = array(
'title' => t('Read article'),
'page callback' => 'my_module_ws_article_read',
'file' => 'my_module.ws.inc,'
'page arguments' => array(2),
'access arguments' => array('access content'),
'type' => MENU_CALLBACK,
);
// Webservice de mise à jour d'un article
$items['api/article/%/update'] = array(
'title' => t('Update article'),
'page callback' => 'my_module_ws_article_update',
'file' => 'my_module.ws.inc,'
'page arguments' => array(2),
'access arguments' => array('access content'),
'type' => MENU_CALLBACK,
);
return $items;
}
Explications :
api/
.my_module_ws_article_read()
et my_module_ws_article_update()
du fichier my_module.ws.inc
.Si le premier webservice doit retourner le nid, le titre, l'URL et le contenu de l'article, il pourrait s"implémenter ainsi :
// my_module.ws.inc
/**
* Retourne un article au format JSON.
*
* @param int $nid Nid de l'article
* @return string Le nid, le titre, l'URL et le contenu de l'article, au format JSON
*/
function my_module_ws_article_read($nid) {
global $language;
$data = array();
// Requête autorisées
drupal_add_http_header('Access-Control-Allow-Origin', "*");
drupal_add_http_header('Access-Control-Allow-Methods', "GET, OPTIONS");
drupal_add_http_header('Access-Control-Allow-Headers', "Origin, X-Requested-With, Content-Type, Accept, Authorization, X-Auth-Token");
// Format du flux de sortie
drupal_add_http_header('Content-Type', 'application/json');
if ($_SERVER['REQUEST_METHOD'] == 'GET') {
$article = node_load($nid);
if (!empty($article) && $article->type == 'article') {
$view = node_view($article);
$title = (!empty($article->title_field[$language->language])) ? $article->title_field[$language->language][0]['value'] : '';
$content = (!empty($view['field_content'])) ? render($view['field_content'])) : '';
$data = array(
'nid' => intval($nid),
'url_alias' => drupal_get_path_alias('node/'. $nid),
'title' => $title,
'content' => $content
);
} else {
http_response_code('404');
$data['error'] = 'Article introuvable (nid: ' . $nid . ').';
}
echo drupal_json_encode($data);
}
}
Explications :
GET
ou OPTIONS
depuis n'importe quel origine),
ainsi que le format de sortie (ici JSON
).content
depuis la vue.Remarque :
Dans cet exemple, le webservices accepte les requêtes de type OPTIONS
.
Cela peut être nécessaire lors de l'utilisation de certains framework (ex: Sencha).
Pour ces requêtes, le contenu n'a pas besoin d'être renvoyé, d'où la condition
($_SERVER['REQUEST_METHOD'] == 'GET')
.
Si vous n'en avez pas besoin il est tout à fait possible de l'enlever.
Voici le code du second webservice, pour mettre à jour le titre et le contenu de l'article.
Pour simplifier, on considère que toute la mise à jour de l'article est déportée dans une
fonction _my_module_article_update($article, $title, $content)
:
// my_module.ws.inc
/**
* Met à jour un article.
*
* @param int $nid Nid de l'article
* @return string 'OK' si la mise à jour a réussi, un message d'erreur sinon.
*/
function my_module_ws_article_update($nid)
{
$data = array();
// Requête autorisées
drupal_add_http_header('Access-Control-Allow-Origin', "*");
drupal_add_http_header('Access-Control-Allow-Methods', "PUT, OPTIONS");
drupal_add_http_header('Access-Control-Allow-Headers', "Origin, X-Requested-With, Content-Type, Accept, Authorization, X-Auth-Token");
// Format du flux de sortie
drupal_add_http_header('Content-Type', 'application/json');
$article = node_load($nid);
if (!empty($article) && $article->type == 'article') {
// Récupération du corps de la requête
$request_body_json = file_get_contents('php://input');
$request_params = json_decode($request_body_json);
$title = isset($request_params->title) ? $request_params->title : null;
$content = isset($request_params->content) ? $request_params->content : null;
if ($title != null || $content != null) {
// Mise à jour de l'article
$status_msg = _my_module_article_update($article, $title, $content);
// Message de retour (ex: 'OK' si réussite, message d'erreur sinon)
$data['content'] = $status_msg;
} else {
http_response_code('400');
$data['error'] = 'Les données de mise à jour sont invalides, ou le json est mal formé (' . $request_params . ').';
}
} else {
http_response_code('404');
$data['error'] = 'Article introuvable (nid: ' . $nid . ').';
}
echo drupal_json_encode($data);
}
Explications :
{
"title": "Mon article",
"content": "<p>Le contenu HTML de mon article</p>"
}
Remarques :
Modifié le :
Publié le :
Drush propose déjà pas mal de fonctionnalités. Il est en plus extensible. Voici comme ajouter votre propre script.
Créez un fichier my_module.drush.inc
, et implémentez-y le hook_drush_command()
:
// my_module.drush.inc
/**
* Implements hook_drush_command().
*/
function my_module_drush_command()
{
$items = array();
$items['say_hello'] = array(
'description' => t('Say "Hello"'),
'arguments' => array(
'who' => t('Who are you talking to ?'),
),
'options' => array(
'punctuation' => 'Which punctuation do you use ? (optional, if not provided, use ".")',
),
'examples' => array(
'drush say_hello Boby' => 'Says : "Hello Boby."',
'drush mmsh Boby punctuation="!"' => 'Says : "Hello Boby !"',
'drush mmsh Boby' => 'Uses the alias and says : "Hello Boby."',
),
'aliases' => array('mmsh'),
'bootstrap' => DRUSH_BOOTSTRAP_DRUSH,
);
return $items;
}
Explications :
say_hello
. La fonction appelée derrière sera donc drush_my_module_say_hello()
.My_Module_Say_Hello
.
On peut appeler le script avec ou sans cet alias :drush say_hello
drush mmsh
Le script pourrait être implémenté ainsi :
// my_module.drush.inc
/**
* Dit bonjour.
* @param string $who Nom de la personne à saluer
*/
function drush_logres_business_say_hello($who) {
$start = new DateTime();
if (empty($who)) {
echo 'Say "Hello" to who ?';
} else {
// Récupération des options
$punctuation = drush_get_option('punctuation');
$punctuation = (!empty($punctuation)) ? ' ' . $punctuation : '.';
// Traitement
echo 'Hello ' . $who . $punctuation;
}
// Affichage du temps d'exécution
$end = new DateTime();
echo "\n" . t('Duration : !duration', array('!duration' => $end->diff($start)->format('%hH %imin %ss')) ) . "\n";
}
Explications :
Modifié le :
Publié le :
Pour gérer certains paramètres propres à votre site, vous utilisez souvent les fonctions
variable_get()
et variable_set()
de Drupal.
Drupal fournit une API pour pouvoir très rapidement créer un formulaire d'édition pour ces variables :
Voici les différentes étapes pour ajouter une page de configuration.
Pour que Drupal référence vos nouvelles pages, déclarez-les dans un hook_menu()
:
// mon_module.module
/**
* Implements hook_menu().
*/
function my_module_menu()
{
// Page racine "Mon site"
$items['admin/config/mon_site'] = array(
'title' => t('My site'),
'description' => t('Administration of the site.'),
'page callback' => 'system_admin_menu_block_page',
'access arguments' => array('administer mysite'),
'position' => 'right',
'file' => 'system.admin.inc',
'file path' => drupal_get_path('module', 'system'),
);
// Page "Général"
$items['admin/config/mon_site/general'] = array(
'title' => t('General'),
'description' => t('Adjust global settings.'),
'page callback' => 'drupal_get_form',
'page arguments' => array('my_module_admin_form_site_general'),
'access arguments' => array('administer mysite'),
'file' => 'my_module.pages.inc',
'type' => MENU_LOCAL_TASK,
);
// Page "Webservices"
$items['admin/config/mon_site/webservices'] = array(
'title' => t('General'),
'description' => t('Adjust global settings.'),
'page callback' => 'drupal_get_form',
'page arguments' => array('my_module_admin_form_site_ws'),
'access arguments' => array('administer mysite'),
'file' => 'my_module.pages.inc',
'type' => MENU_LOCAL_TASK,
);
return $items;
}
Explications :
Mon site
).Général
et Webservices
).my_module_admin_form_site_general()
et my_module_admin_form_site_ws()
.my_module.pages.inc
.administer mysite
.// my_module.page.inc
/**
* Form callback: administration variables
*/
function my_module_admin_form_site_general() {
// Création d'un fieldset
$form['search'] = array(
'#type' => 'fieldset',
'#title' => t('Search'),
);
// Ajout d'un champ texte dans ce fieldset
$form['search']['my_module_nb_results_per_page'] = array(
'#type' => 'textfield',
'#title' => t('Number of results per page'),
'#default_value' => variable_get('my_module_nb_results_per_page'),
'#description' => t('Some description'),
);
// Ajout d'une liste déroulante dans ce fieldset
$options = array(
'value1' => t('Label 1'),
'value2' => t('Label 2'),
);
$form['search']['my_module_first_result'] = array(
'#type' => 'select',
'#title' => t('First result'),
'#options' => $options,
'#default_value' => variable_get('my_module_first_result', 0),
'#description' => t('Some description'),
);
return system_settings_form($form);
}
Explications :
fieldset
correspond à celui HTML, il permet de regrouper des champs.$form
.variable_get()
).Remarque :
Le formulaire décrit dans cette fonction fonctionne exactement comme tout autre formulaire Drupal. Vous pouvez y ajouter des règles de validation de champs, ...
Publié le :
Si vous avez deux versions de votre site, une desktop et une autre mobile, vous voudrez probablement qu'un utilisateur sur mobile soit automatiquement redirigé vers la version adaptée.
Cette configuration peut être définie au niveau de vos virtualhost.
Imaginons que vous ayez deux virtualhost basiques, un pour chaque version.
<VirtualHost *:80>
DocumentRoot /var/www/site_desktop
ServerName www.mon-site.com
<Directory /var/www/site_desktop>
Options FollowSymLinks
Order allow,deny
allow from all
</Directory>
</VirtualHost>
<VirtualHost *:80>
DocumentRoot /var/www/site_mobile
ServerName m.mon-site.com
<Directory /var/www/site_mobile>
Options FollowSymLinks
Order allow,deny
allow from all
</Directory>
</VirtualHost>
Si vous voulez rediriger un utilisateur qui accède au site desktop via son mobile, vers le site mobile, le premier virtualhost deviendra :
<VirtualHost *:80>
DocumentRoot /var/www/site_desktop
ServerName www.mon-site.com
<IfModule mod_rewrite.c>
RewriteEngine on
# Si le client est un navigateur mobile
RewriteCond %{HTTP_USER_AGENT} mobi [NC]
# Redirection vers le site mobile
RewriteRule ^(.*)$ http://m.mon-site.com [R=301,L]
</IfModule>
<Directory /var/www/site_desktop>
Options FollowSymLinks
Order allow,deny
allow from all
</Directory>
</VirtualHost>
Explications :
HTTP_USER_AGENT
) contient la chaîne mobi
, on applique la règle de redirection,
vers le site mobileRemarque :
Vous pouvez bien sûr faire l'inverse (mobile vers desktop), en ajoutant un !
:
<VirtualHost *:80>
DocumentRoot /var/www/site_mobile
ServerName m.mon-site.com
<IfModule mod_rewrite.c>
RewriteEngine on
# Si le client n'est pas un navigateur mobile
RewriteCond %{HTTP_USER_AGENT} !mobi [NC]
# Redirection vers le site desktop
RewriteRule ^(.*)$ http://www.mon-site.com [R=301,L]
</IfModule>
<Directory /var/www/site_mobile>
Options FollowSymLinks
Order allow,deny
allow from all
</Directory>
</VirtualHost>
Selon votre architecture ou vos besoins, vous serez sans doute amené à ajouter d'autres conditions (RewriteCond
) avant
la règle de réécriture (RewriteRule
) :
# 1. Si le client n'est pas sur iPad
RewriteCond %{HTTP_USER_AGENT} !ipad [NC]
# 2. Si le nom de domaine appelé ne commence pas par "m."
RewriteCond %{HTTP_HOST} !^m\..*
# 3. Si l'URL demandée ne commence pas par "/api/mobile/"
RewriteCond %{REQUEST_URI} !(/api/mobile/)
Explications :
!ipad|tablet [NC]
.
Plus d'informations sur le site Mozilla.En général, un site mobile ne propose pas toutes les fonctionnalités du site desktop. De plus, en fonction de la taille de l'appareil de l'utilisateur, ce dernier peut préférer utiliser le site desktop.
L'idéal est de proposer un lien vers la version desktop sur le site mobile (éventuellement l'inverse).
Le problème, c'est qu'avec les règles de redirection définies précédemment, l'utilisateur sera automatiquement redirigé en cliquant sur le lien.
Une solution est d'ajouter un paramètre à l'url, et de stocker l'information en cookie.
Le lien depuis le site mobile vers le site desktop aura par exemple pour URL http://www.mon-site?mobile=0
.
Pour cela, la règle concernant la redirection doit être améliorée :
<IfModule mod_rewrite.c>
RewriteEngine on
# Si l'URL contient le paramètre 'mobile', égal à 1
RewriteCond %{QUERY_STRING} (^|&)mobile=1(&|$)
# On ajoute un cookie
RewriteRule ^ - [CO=mobile:1:%{HTTP_HOST}]
# Si l'URL contient le paramètre 'mobile', égale à 0
RewriteCond %{QUERY_STRING} (^|&)mobile=0(&|$)
# On ajoute un cookie
RewriteRule ^ - [CO=mobile:0:%{HTTP_HOST}]
# Si l'URL contient le paramètre 'mobile', égale à 1
RewriteCond %{QUERY_STRING} (^|&)mobile=0(&|$)
# On saute la prochaine RewriteRule
RewriteRule ^ - [S=1]
# Si le client est un navigateur mobile
RewriteCond %{HTTP_USER_AGENT} mobi [NC]
# Si le cookie n'est pas égal à 0
RewriteCond %{HTTP:Cookie} !\mobile=0(;|$)
# Redirection vers le site desktop
RewriteRule ^(.*)$ http://m.mon-site.com [R=301,L]
</IfModule>
Modifié le :
Publié le :
Lorsque vous utilisez les alias d'URL de Drupal et pathauto
pour les générer automatiquement,
il vous arrive peut-être ce problème.
pathauto
générer son alias URL par défaut.Le problème se produit probablement lorsque vous avez un site multilingue, que vous modifiez le pattern de génération de l'URL alors que vous avez déjà des contenus existants, et que vous utiliser l'option Créer un nouvel alias et conserver l'alias existant.
Une solution semble de modifier la fonction path_load()
du fichier path.inc
du cœur de Drupal.
Ajoutez cette ligne à la requête récupérant l'alias d'URL :
->orderBy('pid', 'DESC')
De cette manière, vous êtes sûr que Drupal choisira le dernier alias généré (= alias personnalisé) et non pas l'ancien (automatique).
Remarque :
Je n'ai pas réussi à isoler le problème sur une installation vierge, avec un minimum de modules. Je l'ai reproduit sur deux sites très similaires assez importants. Dans les deux cas, l'ajout de cette ligne à solutionner le problème.
Modifié le :
Publié le :
Pour restreindre l'accès à un répertoire ou à un site entier, vous pouvez le protéger par un mot de passe :
Pour pouvoir utiliser cette fonctionnalité d'Apache, vous aurez besoin des modules suivants : mod_auth_basic, mod_authn_file et mod_authz_user.
Vous devez également générer un fichier de mots passe, via l'utilitaire htpasswd fourni avec Apache.
Il se trouve probablement dans le répertoire bin/
de votre installation apache.
Pour cela, lancez la commande suivante :
htpasswd -c /chemin/vers/un/repertoire/protege/passwords nom_utilisateur
Remarques :
passwords
dans /usr/local/apache/passwd/
.-c
.Le fichier généré pourra ressembler à a ça :
user1:.G.h/4RfP93fd
user2:RlPRITNDHefEpl
Cette configuration peut se faire au niveau de votre virtualhost :
<VirtualHost *:80>
DocumentRoot /var/www/mon_site
ServerName mon-site.com
<Directory /var/www/mon_site>
Options FollowSymLinks
Order allow,deny
allow from all
</Directory>
<Directory /var/www/mon_site/protected>
Order allow,deny
Allow from all
AllowOverride None
AuthType Basic
# Intitulé de la mire de connexion
AuthName "Acces restreint"
# Type de provider
AuthBasicProvider file
# Fichier contenant les utilisateurs et leur mots de passe cryptés
AuthUserFile /chemin/vers/mon/fichier/passwords
Require valid-user
</Directory>
</VirtualHost>
Explications :
mon_site/
) et ses sous-répertoires sont accessibles à tousprotected/
lui, est protégé par un mot de passeRemarque :
Pour plus de sécurité, il est également judicieux de passer le site en HTTPS.
Si vous voulez gérer des accès avec plusieurs utilisateurs, il est possible de les organiser par groupe.
Vous pourrez ainsi définir que tel utilisateur appartient à tel ou tel groupe, et que tel ou tel groupe à accès à tel ou tel répertoire.
Le virtualhost est alors un peu modifié :
<VirtualHost *:80>
DocumentRoot /var/www/mon_site
ServerName mon-site.com
<Directory /var/www/mon_site>
Options FollowSymLinks
Order allow,deny
allow from all
</Directory>
<Directory /var/www/mon_site/protected>
Order allow,deny
Allow from all
AllowOverride None
AuthType Basic
# Intitulé de la mire de connexion
AuthName "Acces restreint"
# Type de provider
AuthBasicProvider file
# Fichier contenant les utilisateurs et leur mots de passe cryptés
AuthUserFile /chemin/vers/mon/fichier/passwords
# Fichier contenant les groupes et leurs utilisateurs
AuthGroupFile /chemin/vers/mon/fichier/groups
Require group_name
</Directory>
</VirtualHost>
Explication :
La propriété AuthUserFile
a été ajoutée, pour définir où se trouve le fichier contenant la liste des groupes.
La règle de restriction a également changé, elle définit maintenant le nom du groupe ayant accès au répertoire.
Remarques :
Pour utiliser ce système de groupes, vous aurez besoin du module mod_authz_groupfile
d'Apache.
Le fichier groups
contient quelque chose comme ça :
group1: user1 user2
group2: user2 user3
Contrairement au fichier passwords
, il n'est pas crypté et peut donc être créé via un éditeur de texte classique.
Modifié le :
Publié le :
Par défaut, Drupal rend la variable $scripts
disponible dans votre html.tpl.php
.
Elle contient toutes les lib javascript ainsi que tous le code javascript inline
que vous avez ajouté à Drupal via :
.info
de votre thème (mon_theme.info
)drupal_add_js()
Vous avez donc quelque chose comme ça au début de votre fichier html.tpl.php
:
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="utf-8">
<title><?php print $head_title; ?></title>
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<?php print $styles; ?>
<?php print $scripts; ?>
<?php print $head; ?>
</head>
Si vous déplacez <?php print $scripts; ?>
tout à la fin du body,
vous aurez sûrement des problèmes, car certains modules vont vouloir
utiliser des lib javascript en amont (par exemple jQuery).
Un solution consiste à laissez $scripts
où il se trouve et
à créer une nouvelle variable $footer_scripts
.
Elle sera ajoutée à la fin du body et contiendra tout le javascript "non natif" dont
aurez besoin pour votre site :
<?php if (isset($footer_scripts)) { print $footer_scripts; } ?>
</body>
</html>
Voici les différentes étapes à suivre pour pouvoir utiliser cette variable.
Pour cela, utilisez le hook_process_html()
:
// mon_module.module
/**
* Implements hook_process_html().
*/
function mon_module_process_html(&$variables) {
// Ajout des scripts JS à mettre en pied de page dans la variable $footer_scripts
$variables['footer_scripts'] = drupal_get_js('footer');
}
Explication :
La variable $footer_scripts
aura pour valeur le code HTML permettant d'inclure tout le code JS
dont le scope est footer
.
Pour cela, utilisez le hook_preprocess_page()
:
// mon_theme/template.php
/**
* Implements hook_preprocess_page().
*/
function mon_theme_preprocess_page(&$vars) {
// Ajout de code javascript inline
$js = 'alert("Ceci est du code Javascript.");';
drupal_add_js($js, array('type' => 'inline', 'scope' => 'footer'));
// Exemple d'ajout de code javascript externe
drupal_add_js(
'http://maps.googleapis.com/maps/api/js?v=3',
array('type' => 'external', 'scope' => 'footer')
);
}
Modifié le :
Publié le :
Pour pouvoir utiliser le SSL/TLS et passer votre site en HTTPS, les paquets suivants doivent être installés sur votre serveur :
De plus, vous aurez besoin d'un certificat pour effectuer le chiffrement.
Celui-ci doit être obtenu auprès d'une autorité de certification et stocké dans un endroit sécurisé de votre serveur.
À des fins de test, ou pour un site privé, il est possible de générer son propre certificat et de l'auto-signer.
Pour cela vous devez probablement être connecté en root sur votre serveur (ou sudo) :
# Génération d'une clé privée
openssl genrsa -out my_certif.key 2048
# Génération d'un fichier CSR
openssl req -new -key my_certif.key -out my_certif.csr
# Génération de la clé auto-signée
openssl x509 -req -days 365 -in my_certif.csr -signkey my_certif.key -out my_certif.crt
Vous avez maintenant deux fichiers particuliers :
my_certif.crt
my_certif.key
Voici les paramètres standards d'Apache pour utiliser le HTTPS :
##
## SSL Global Context
##
## All SSL configuration in this context applies both to
## the main server and all SSL-enabled virtual hosts.
##
LoadModule ssl_module modules/mod_ssl.so
#
# When we also provide SSL we have to listen to the
# the HTTPS port in addition.
#
Listen 443
# Necessary if you have several virtual hosts on 443 port
NameVirtualHost *:443
# Pass Phrase Dialog:
# Configure the pass phrase gathering process.
# The filtering dialog program ('builtin' is a internal
# terminal dialog) has to provide the pass phrase on stdout.
SSLPassPhraseDialog builtin
# Inter-Process Session Cache:
# Configure the SSL Session Cache: First the mechanism
# to use and second the expiring timeout (in seconds).
SSLSessionCache shmcb:/var/cache/mod_ssl/scache(512000)
SSLSessionCacheTimeout 300
# Semaphore:
# Configure the path to the mutual exclusion semaphore the
# SSL engine uses internally for inter-process synchronization.
SSLMutex default
# Pseudo Random Number Generator (PRNG):
# Configure one or more sources to seed the PRNG of the
# SSL library. The seed data should be of good random quality.
# WARNING! On some platforms /dev/random blocks if not enough entropy
# is available. This means you then cannot use the /dev/random device
# because it would lead to very long connection times (as long as
# it requires to make more entropy available). But usually those
# platforms additionally provide a /dev/urandom device which doesn't
# block. So, if available, use this one instead. Read the mod_ssl User
# Manual for more details.
SSLRandomSeed startup file:/dev/urandom 256
SSLRandomSeed connect builtin
#SSLRandomSeed startup file:/dev/random 512
#SSLRandomSeed connect file:/dev/random 512
#SSLRandomSeed connect file:/dev/urandom 512
#
# Use "SSLCryptoDevice" to enable any supported hardware
# accelerators. Use "openssl engine -v" to list supported
# engine names. NOTE: If you enable an accelerator and the
# server does not start, consult the error logs and ensure
# your accelerator is functioning properly.
#
SSLCryptoDevice builtin
#SSLCryptoDevice ubsec
Très souvent, apache propose un fichier de configuration dédié à ce paramétrage (ex: ssl.conf
).
Vérifiez qu'il est bien inclus par le httpd.conf
, ou créez-le.
Il faut ensuite configurer un virtualhost pour le port 443
(celui par défaut pour le HTTPS) :
<VirtualHost *:443>
DocumentRoot /var/www/mon_site
ServerName mon-site.com
# SSL Engine Switch:
# Enable/Disable SSL for this virtual host.
SSLEngine on
# SSL Protocol support:
# List the enable protocol levels with which clients will be able to
# connect. Disable SSLv2 access by default:
SSLProtocol all -SSLv2
# SSL Cipher Suite:
# List the ciphers that the client is permitted to negotiate.
# See the mod_ssl documentation for a complete list.
SSLCipherSuite ALL:!ADH:!EXPORT:!SSLv2:RC4+RSA:+HIGH:+MEDIUM:+LOW
# Server Certificate:
# Point SSLCertificateFile at a PEM encoded certificate. If
# the certificate is encrypted, then you will be prompted for a
# pass phrase. Note that a kill -HUP will prompt again. A new
# certificate can be generated using the genkey(1) command.
SSLCertificateFile /chemin/vers/certifs/my_certif.crt
# Server Private Key:
# If the key is not combined with the certificate, use this
# directive to point at the key file. Keep in mind that if
# you've both a RSA and a DSA private key you can configure
# both in parallel (to also allow the use of DSA ciphers, etc.)
SSLCertificateKeyFile /chemin/vers/certifs/my_certif.key
# SSL Protocol Adjustments:
# The safe and default but still SSL/TLS standard compliant shutdown
# approach is that mod_ssl sends the close notify alert but doesn't wait for
# the close notify alert from client. When you need a different shutdown
# approach you can use one of the following variables:
# o ssl-unclean-shutdown:
# This forces an unclean shutdown when the connection is closed, i.e. no
# SSL close notify alert is send or allowed to received. This violates
# the SSL/TLS standard but is needed for some brain-dead browsers. Use
# this when you receive I/O errors because of the standard approach where
# mod_ssl sends the close notify alert.
# o ssl-accurate-shutdown:
# This forces an accurate shutdown when the connection is closed, i.e. a
# SSL close notify alert is send and mod_ssl waits for the close notify
# alert of the client. This is 100% SSL/TLS standard compliant, but in
# practice often causes hanging connections with brain-dead browsers. Use
# this only for browsers where you know that their SSL implementation
# works correctly.
# Notice: Most problems of broken clients are also related to the HTTP
# keep-alive facility, so you usually additionally want to disable
# keep-alive for those clients, too. Use variable "nokeepalive" for this.
# Similarly, one has to force some clients to use HTTP/1.0 to workaround
# their broken HTTP/1.1 implementation. Use variables "downgrade-1.0" and
# "force-response-1.0" for this.
SetEnvIf User-Agent ".*MSIE.*" \
nokeepalive ssl-unclean-shutdown \
downgrade-1.0 force-response-1.0
<Directory /var/www/mon_site>
#Options FollowSymLinks
#AllowOverride None
Order allow,deny
allow from all
</Directory>
</VirtualHost>
Explication :
Le virtualhost contient les informations habituelles (nom de domaine, répertoire racine, ...) mais également des configurations propres au SSL/TLS et les chemins vers les certificats que vous avez générés ou obtenus auprès d'une Autorité de certification.
De plus, il répond sur le port 443
et non le 80
habituel.
Pour éviter que les utilisateurs du site n'aient aucune réponse lorsqu'ils tapent
(http://)mon-site.com
dans leur navigateur, il est judicieux de rediriger les accès en HTTP vers l'URL en HTTPS.
Pour cela, ajoutez un second virtualhost gérant la redirection :
<VirtualHost *:80>
DocumentRoot /var/www/mon_site
ServerName mon-site.com
Redirect permanent / https://mon-site.com:443/
</VirtualHost>
Modifié le :
Publié le :
Lorsque vous affichez plusieurs points (= marqueurs) sur une Google Map, vous voulez en général que la carte soit suffisamment dézoomée pour permettre de les afficher tous.
Pour afficher une carte avec plusieurs marqueurs, vous pouvez suivre le deuxième exemple de cet article.
Pour rappel, le code javascript utilisé est celui-ci :
function initMap() {
// Création de la carte
var paris_latlong = {lat: 48.866667, lng: 2.333333};
var map = new google.maps.Map(document.getElementById('map'), {
zoom: 10,
center: paris_latlong
});
// Ajout de marqueurs
var markerList = [
{ lat: 48.837347, lng: 2.291787, title: "Marqueur n°1" },
{ lat: 48.879681, lng: 2.379958, title: "Marqueur n°2" },
{ lat: 48.822399, lng: 2.498793, title: "Marqueur n°3" }
];
for (var i = 0, length = markerList.length; i < length; i++) {
var latLng = new google.maps.LatLng(markerList[i].lat, markerList[i].lng);
var marker = new google.maps.Marker({
position: latLng,
map: map,
title: markerList[i].title
});
}
}
Par défaut, le centre et le niveau de zoom sont définis lors de l'initialisation de la carte. Il serait plus intéressant que ces valeurs soit calculées automatiquement.
Pour cela Google Map permet de définir les limites de la carte : les bounds.
Le code est alors légèrement modifié :
function initMap() {
// Création de la carte
var paris_latlong = {lat: 48.866667, lng: 2.333333};
var map = new google.maps.Map(document.getElementById('map'), {
zoom: 10,
center: paris_latlong
});
// Initialisation des limites de la carte
var bounds = new google.maps.LatLngBounds();
// Ajout de marqueurs
var markerList = [
{ lat: 48.837347, lng: 2.291787, title: "Marqueur n°1" },
{ lat: 48.879681, lng: 2.379958, title: "Marqueur n°2" },
{ lat: 48.822399, lng: 2.498793, title: "Marqueur n°3" }
];
for (var i = 0, length = markerList.length; i < length; i++) {
var latLng = new google.maps.LatLng(markerList[i].lat, markerList[i].lng);
var marker = new google.maps.Marker({
position: latLng,
map: map,
title: markerList[i].title
});
// Ajout des coordonnées du marqueur aux limites
bounds.extend(markerList[i].lat, markerList[i].lng);
}
// Application des limites à la carte
map.fitBounds(bounds);
}
Explications :
LatLngBounds
) et on lui transmet les coordonnées de
tous les marqueurs affichés.On obtient alors ce résultat :
Si on enlève le 3ème marqueur, la carte est automatiquement recentrée :
Le problème de cette technique, c'est que si vous n'affichez qu'un seul point, le zoom sera très important :
La solution la plus simple est d'ajouter un zoom maximal à la carte, lors de son initialisation :
var map = new google.maps.Map(document.getElementById('map'), {
zoom: 10,
center: paris_latlong,
maxZoom: 11
});
Le gros problème, c'est que l'utilisateur sera bloqué et ne pourra pas zoomer plus que la valeur que vous aurez définie.
À la place d'utiliser le zoom maximal, on peut modifier le zoom automatiquement en fonction des limites de la carte :
var zoomChangeBoundsListener = google.maps.event.addListener(map, 'bounds_changed', function(event) {
google.maps.event.removeListener(zoomChangeBoundsListener);
map.setZoom(Math.min( 11, map.getZoom()));
});
Explications :
bounds_changed
fourni par Google Map.Remarque :
Il faut ajouter ce code après l'initialisation de la carte, mais avant d'appeler la méthode fitBounds()
.
Sans quoi l'écouteur ne captera pas l'évènement.
Modifié le :
Publié le :
Voici une liste d'utilisations courantes de l'API Google Map :
Pour pouvoir utiliser l'API Google, vous devez tout d'abord avoir un compte Google et générer une clé.
*.mon-site.com
, 127.0.0.1
, localhost
, ...)Copiez la clé de l'API :
Voici le code HTML/Javascript pour afficher une carte simple, sans marqueur :
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<style>
#map {
height: 400px;
width: 600px;
}
</style>
</head>
<body>
<h3>My Google Maps Demo</h3>
<div id="map"></div>
<script>
function initMap() {
var paris_latlong = {lat: 48.866667, lng: 2.333333};
var map = new google.maps.Map(document.getElementById('map'), {
zoom: 10,
center: paris_latlong
});
}
</script>
<script async defer src="https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY&callback=initMap"></script>
</body>
</html>
Explications :
div
, dans lequel la carte sera générée en javascript.
Cet élément doit avoir une largeur et une hauteur (cf. CSS).initMap()
est appelée en callback.Remarque :
Comme présenté dans la documentation officielle, Google conseille d'indiquer la clé d'API lors de l'appel à la librairie javascript. Il est toutefois possible de s'en passer (en phase de test par exemple), en modifiant l'URL :
<script async defer src="https://maps.googleapis.com/maps/api/js?callback=initMap"></script>
Si on veut afficher des marqueurs, le code javascript devient :
function initMap() {
// Création de la carte
var paris_latlong = {lat: 48.866667, lng: 2.333333};
var map = new google.maps.Map(document.getElementById('map'), {
zoom: 10,
center: paris_latlong
});
// Ajout de marqueurs
var markerList = [
{ lat: 48.837347, lng: 2.291787, title: "Marqueur n°1" },
{ lat: 48.879681, lng: 2.379958, title: "Marqueur n°2" },
{ lat: 48.822399, lng: 2.498793, title: "Marqueur n°3" }
];
for (var i = 0, length = markerList.length; i < length; i++) {
var latLng = new google.maps.LatLng(markerList[i].lat, markerList[i].lng);
var marker = new google.maps.Marker({
position: latLng,
map: map,
title: markerList[i].title
});
}
}
Explications :
Remarque :
Pour centrer automatiquement la carte sur ces points, vous pouvez suivre cet article.
Il est agréable d'utiliser ses propres icônes de marqueur.
Pour cela, il n'y a quasiment rien à ajouter :
function initMap() {
// Création de la carte
var paris_latlong = {lat: 48.866667, lng: 2.333333};
var map = new google.maps.Map(document.getElementById('map'), {
zoom: 10,
center: paris_latlong
});
// Ajout du marqueur
var marker = new google.maps.Marker({
position: paris_latlong,
map: map,
title: 'Paris',
icon: 'http://www.devnotebook.fr/public/articles/api-diverses/utiliser-l-api-google-map-06.png'
});
}
Explication :
On a juste ajouté l'attribut icon
au marqueur.
Pour afficher une bulle d'information (InfoWindow
)
au clic sur le marqueur, voici ce que devient le code javascript :
function initMap() {
// Création de la carte
var paris_latlong = {lat: 48.866667, lng: 2.333333};
var map = new google.maps.Map(document.getElementById('map'), {
zoom: 10,
center: paris_latlong
});
// Ajout du marqueur
var marker = new google.maps.Marker({
position: paris_latlong,
map: map,
title: 'Paris'
});
// Ajout d'une InfoWindow
var htmlContent = '<div class="info-window-container">';
htmlContent += ' <h2>Mon marqueur</h2>';
htmlContent += ' <p>Une petite description</p>';
htmlContent += ' <p><a href="https://developers.google.com/maps/documentation/javascript/infowindows">Lien vers l\'API Google</a></p>';
htmlContent += '</div>';
var infoWindow = new google.maps.InfoWindow({
content: htmlContent
});
marker.addListener('click', function() {
infoWindow.open(map, marker);
});
}
Explications :
InfoWindow
avec pour contenu ce code HTML.Modifié le :
Publié le :
Si vous créez un site proposant des logements, vous aurez sans doute besoin d'afficher leurs Diagnostics de Performance Énergétique (DPE) et leurs Émissions de Gaz à effet de Serre (GES) :
Si vous avez déjà une image ou un document PDF représentant ces graphiques pas de problème. Sinon, voici une petite API PHP pour les générer en HTML/CSS.
L'archive contient une classe PHP, un fichier CSS, un répertoire d'images et un fichier index.php
.
Cette API est inspirée de celle proposée sur ce site : http://dpe-ges.c-prod.com/telechargement/.
Modifié le :
Publié le :
Dans une feuille CSS, si vous voulez appliquer un style particulier à élément, pour un petit écran, vous faites quelque chose comme ça :
@media only screen and (max-width: 36.9375em) {
.col {
float: none;
width: 100%;
}
}
Il est possible d'utiliser une syntaxe similaire en Javascript :
if (window.matchMedia != undefined
&& window.matchMedia('(max-width: 53.6875em)').matches) {
alert('Mon écran est tout petit !').
}
Explication :
On vérifie que le navigateur comprend cette syntaxe et si oui on ajoute une condition sur la taille de la fenêtre.
Modifié le :
Publié le :
Il est utile de numéroter les différentes versions d'un site web, particulièrement quand il doit être déployé sur plusieurs environnements (ex: dev, recette, production, ...). Ce numéro peut correspondre à un tag svn par exemple.
Pour savoir dans quelle version se trouve chaque instance, voici comment l'afficher dans la page d'information système de Drupal en back-office :
Pour modifier le formulaire présent sur la page d'information système,
il faut utiliser le hook_field_widget_form_alter()
.
Dans ce hook, vous pouvez modifier un formulaire pour y ajouter/enlever des champs.
Dans notre cas, l'objectif est d'ajouter un champ version
, non modifiable :
/**
* Implements hook_field_widget_form_alter().
*/
function mon_module_form_alter(&$form, &$form_state, $form_id) {
switch($form_id) {
case 'system_site_information_settings':
$form['site_information']['mon_site_version'] = array(
'#type' => 'textfield',
'#title' => t('Version'),
'#value' => variable_get('mon_site_version'),
'#attributes' => array('readonly' => 'readonly')
);
break;
default:
break;
}
}
Explications :
textfield
, avec pour libellé Version
,
en lecture seule (= avec l'attribut HTML readonly
).mon_site_version
.Remarque :
Pour mettre à jour le numéro de version automatiquement à la mise à jour du module, voir l'exemple dans cet article : hook_update() dans D7.
Modifié le :
Publié le :
Les micro-données permettent de renforcer la sémantique de votre code HTML. Elles sont interprétées par les principaux moteurs de recherche (Google, Yahoo, Bing, ...).
Par exemple, si vous présentez un film, vous aller pouvoir indiquer que telle valeur est le titre du film, telle autre est sa date de sortie, son réalisateur, ...
C'est ce genre de données qui aide Google à créer les encarts à droite des résultats de recherche :
De très nombreuses choses peuvent être décrites grâce aux micro-données (évènement sportif, adresse, personne, recette de cuisine, ...).
Elles sont de plus organisées avec un système d'héritage (comme en langage objet). Le site schema.org récapitule tout ça de manière arborescente.
Les micro-données se présentent sous forme d'attributs HTML, utilisables sur n'importe quelle balise du DOM à l'intérieur du body.
Exemple :
<p itemtype="http://schema.org/PostalAddress" itemscope="">
<span itemprop="streetAddress">1 Grande Rue</span><br />
<span itemprop="postalCode">75000</span>
<span itemprop="addressLocality">Paris</span>
<meta itemprop="addressCountry" content="FR" />
</p>
Cet exemple va afficher un paragraphe avec les informations suivantes :
1 Grande Rue
75000 Paris
Explications :
itemtype
,
avec pour valeur l'URL du schéma de données à utiliser.
On ajoute également l'attribut itemscope
pour lier la micro-donnée à l'élément p
.span
ici par exemple) à laquelle on ajoute l'attribut itemprop
contenant le nom de la propriété correspondante.meta
autofermante,
dont le contenu est spécifié via l'attribut content
.Pour valider vos micro-données et savoir comment Google les interprétera, il fournit un outil de validation à cette adresse : https://developers.google.com/structured-data/testing-tool/
Le site alsacréations propose un article plus détaillé sur les micro-données.
Modifié le :
Publié le :
Dans Drupal, le hook_update()
permet d'exécuter du code PHP à la mise à jour d'un module.
Imaginons par exemple que vous ayez un numéro de version de votre site web, enregistré dans une variable drupal. Lors de la mise à jour de votre site, vous souhaiter incrémenter ce numéro de version.
Il suffit d'utiliser le hook_update()
, et lorsque le module sera mis à jour,
l'incrémentation sera appliquée automatiquement.
De plus, si vous avez plusieurs hook_update()
, il seront tous exécutés un à un,
dans l'ordre, lors de la mise à jour.
Lors des mises à jour suivantes seuls les nouveaux hooks n'ayant pas encore été exécutés le seront.
Par convention, ce hook doit être utilisé dans le fichier mon_module.install
:
/**
* Update v1.0.1
*/
function mon_module_update_7101() {
variable_set('mon_site_version', '1.0.1');
}
Explication :
Le numéro à la fin du hook correspond à la version du module. Le premier chiffre, par convention, désigne la version majeure de drupal utilisée.
Une fois la nouvelle version du module déployée sur votre site, vous devez lancer la mise à jour. Cela peut-être fait en back-office dans la page de gestion des modules, ou mieux, via une commande drush :
drush updb -y
Imaginons que vous ayez passé cette première mise à jour (7101
) sur l'un de vos sites
(exemple : l'instance de production), et qu'entre temps vous ayez créé deux nouvelles versions du module.
Le fichier mon_module.install
devient :
/**
* Update v1.0.1
*/
function mon_module_update_7101() {
variable_set('mon_site_version', '1.0.1');
}
/**
* Update v1.0.2
*/
function mon_module_update_7102() {
variable_set('mon_site_version', '1.0.2');
// Autres traitements
}
/**
* Update v1.0.3
*/
function mon_module_update_7103() {
variable_set('mon_site_version', '1.0.3');
// Autres traitements
}
Si vous lancez la mise à jour sur votre instance de production,
les fonctions mon_module_update_7102()
et mon_module_update_7103()
seront exécutées dans cet ordre.
La fonction mon_module_update_7101()
elle, ne sera pas exécutée.
Modifié le :
Publié le :
Pour centrer un élément par rapport à son conteneur, il faut utiliser la propriété CSS vertical-align
avec pour valeur middle
:
vertical-align: middle;
Initialement, cette propriété n'était utilisable que dans un tableau, où les cellules ont un display particulier.
Elle est maintenant disponible pour tout élément dont le display est inline-block
.
Voici un exemple avec une image, un bloc de texte et un tableau centrés verticalement par rapport à un conteneur :
Voici le code HTML associé :
<html class="no-js" lang="fr">
<head>
<meta charset="UTF-8">
<title>Centrer verticalement en CSS</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style type="text/css">[...]</style>
</head>
<body>
<div class="container">
<!-- Image -->
<div class="child item-1">
<img src="image.jpg" alt="" />
</div>
<!-- Texte -->
<p class="child item-2">
Duis eget eros neque. Nunc ante ex, scelerisque vehicula orci in, ultrices lacinia lorem. Aenean tincidunt a felis gravida egestas. Suspendisse potenti. Nullam congue commodo euismod. Sed non rutrum orci. Proin eu neque finibus augue aliquam hendrerit in vitae odio. Aenean sagittis mauris quis eros egestas rutrum. Nam tempor ante id lacus placerat dignissim. Aliquam mollis ligula nec justo consequat, id laoreet libero tincidunt. Sed accumsan hendrerit velit et fermentum. Morbi aliquet rhoncus ante vitae posuere. Integer malesuada augue turpis, vel egestas ligula pharetra nec. Quisque ut malesuada mi, non consequat nunc. Nullam posuere, enim quis ullamcorper cursus, lectus nunc pulvinar tellus, in dignissim augue lacus eget sapien.<br />
Sed mollis metus tortor. Proin pulvinar augue sed augue sodales, vel interdum mi interdum.
</p>
<!-- Tableau -->
<table class="child item-3">
<tr>
<th>Colonne 1</th>
<th>Colonne 2</th>
</tr>
<tr>
<td>Valeur 1</td>
<td>Valeur 2</td>
</tr>
<tr>
<td>Valeur 1</td>
<td>Valeur 2</td>
</tr>
</table>
</div>
</body>
</html>
Et le CSS :
img {
max-width: 100%;
}
.container {
display: inline-block; /* Ou block */
line-height: 400px; /* Facultatif si l'un des enfants prend toute la hauteur */
background-color: #EAF3FB;
}
.child {
display: inline-block; /* Obligatoire */
padding: 20px;
line-height: 1; /* Facultatif si le parent n'a pas de line-height. */
vertical-align: middle; /* Obligatoire */
background-color: #FFFFFF;
}
.item-1 {
width: calc(15% - 40px);
}
.item-2 {
width: calc(45% - 40px);
}
.item-3 {
width: calc(40% - 8px); /* Dernier élément donc -40px inutile */
}
Remarque :
Seules les propriétés marquées comme obligatoires/facultatives concernent le centrage vertical.
Bonus :
max-width: 100%;
évite que l'image ne dépasse de son conteneur lorsque la fenêtre est rétrécie en largeurwidth: calc(40% - 40px);
permet de définir la taille de l'élément en pourcentage par rapport à son parent,
tout en utilisant un padding.
En toute logique la somme des padding left et right doit être soustraite au %.
Il est à noter que le dernier élément n'a pas besoin de -40px
, car son padding est intégré automatiquement
lors du calcul de sa largeur. (calc()
est reconnu depuis IE9 seulement)width: calc(40% - 8px);
permet de gérer la marge automatique ajoutée entre les éléments inline-block.
Pour une taille de police non modifiée, un espace fait 4px
de large.
On a 3 éléments, donc 2 espaces, soient les 8px
. Sans cette histoire d'espace width: 40%;
aurait suffit.Modifié le :
Publié le :
Pour pouvoir exporter/importer une base de données PostgreSQL vous devrez sans doute vous connecter avec
l'utilisateur système postgres
.
Ex:
sudo su - postgres
Pour créer un dump de votre base de données utilisez la commande suivante :
pg_dump -U username -h localhost dbname > path/to/dump.sql
Par défaut :
localhost
5432
D'autres options sont possibles.
Remarque :
Même si c'est la valeur par défaut, il est parfois nécessaire de préciser l'hôte dans la commande, pour indiquer au client postgres que vous accédez à la base via une connexion TCP et non PEER.
La commande d'import est similaire à celle de dump :
psql -U username -h localhost dbname < path/to/dump.sql
Par défaut :
localhost
5432
Modifié le :
Publié le :
Si vous développez avec VirtualBox et Apache, il vous êtes peut-être arrivé le même problème.
Vous avez une image ou un fichier CSS servi par Apache. Vous le modifiez et lorsque vous tentez d'y accéder via le navigateur c'est l'ancienne version que vous voyez.
C'est un bug de VirtualBox qui cause le problème (voir le ticket).
Pour l'éviter, ajouter la ligne suivante dans votre configuration d'Apache
(.htaccess
, httpd.conf
, configuration de virtualhost, ...) :
EnableSendfile Off
Remarque : N'oubliez pas de redémarrer Apache après ça.
Modifié le :
Publié le :
Si vous travaillez avec Virtualbox, vos VM ont peut-être le format par défaut proposé par le logiciel : vdi.
Malheureusement beaucoup de serveurs de virtualisation ne reconnaissent pas ce format mais plutôt le vmdk.
Voici une ligne de commande permettant d'effectuer la conversion de la VM de vdi vers vmdk :
vboxmanage clonehd ma_vm.vdi ma_nouvelle_vm.vmdk -format VMDK -variant Fixed,ESX
Remarque :
Vous devez éteindre la VM à copier et stopper Virtualbox avant de lancer la commande.
Modifié le :
Publié le :
Pour connaitre votre distribution et sa version, utilisez la commande suivante :
lsb_release -a
Remarque :
Cette commande n'est pas forcément installée par défaut sur votre machine. Elle se trouve sûrement dans le dépôt officiel et peut être ajoutée grâce à yum ou apt.
Modifié le :
Publié le :
Il arrive de rencontrer des IllegalStateException
lorsqu'on effectue des redirections dans des servlets.
En général ça se produit quand on a déjà envoyé des données ou qu'on a déjà demandé une redirection dans notre requête HTTP.
Pour éviter cela, il suffit d'utiliser la méthode isCommited()
en remplaçant
// ServletResponse response
if (response != null) {
response.sendRedirect("http://www.google.fr");
return;
}
par
// ServletResponse response
if (response != null && !response.isCommited()) {
response.sendRedirect("http://www.google.fr");
return;
}
Modifié le :
Publié le :
Screen permet d'ouvrir plusieurs terminaux dans une même console, de passer de l'un à l'autre et de les mémoriser pour les récupérer plus tard.
Si vous avez une interface graphique sous Linux, vous avez souvent plusieurs consoles d'ouvertes, ou même une seule avec plusieurs onglets.
Mais si vous n'avez pas d'interface graphique, ou si vous accédez à votre machine linux avec PuTTY, vous n'avez pas cette possibilité. C'est là que Screen entre en jeu.
Screen est disponible sur les dépôts standards. Vous pouvez donc l'installez simplement via apt ou yum.
Remarque :
Sous Debian, vous devrez peut-être ajouter des dépôts supplémentaires (Ex: Debian 7.x)
Voici un cas d'utilisation classique de Screen.
Tout ça est possible avec Screen et ces quelques commandes
Accédez au serveur en SSH comme d'habitude, depuis votre machine A au boulot
Créez un nouveau screen en nommant la session :
screen -S ma_session
Exécutez une commande, connecté en user1
Ouvrez un nouveau terminal avec CTRL + A
, puis C
Changez d'utilisateur (user2) et exécutez une commande
Naviguez vers l'autre terminal (connecté en user1) avec CTRL + A
, puis N
Exécutez une commande, connecté en user1
Détachez le screen (il reste actif mais vous n'y êtes plus connecté) avec CTRL + A
, puis D
Éteignez la machine A, retournez à la maison
Allumez votre machine B à la maison, et connectez-vous au serveur en SSH comme d'habitude.
Récupérez votre session screen :
screen -r
Vous récupérez ainsi les deux terminaux dans l'état où vous les avez laissés.
Remarque :
Si vous avez plusieurs sessions en cours, screen -r
vous en affichera la liste.
Il faudra utiliser screen -r nom_de_session
pour choisir celle que vous souhaitez récupérer.
CTRL + D
.
Si c'était le seul, cela quittera screen.CTRL + A
, puis A
CTRL + A
, puis ECHAP
Vous pouvez maintenant naviguer avec les flèches et pageUp
/pageDown
.screen -x ma_session
exit
screen -d ma_session
CTRL + A
puis tapez :nom_session new_nom_session
et validez avec Entrée
Modifié le :
Publié le :
Sous Linux, vous pouvez personnaliser l'invite de commande, pour y mettre un peu de couleur ou modifier ce qu'il y a avant le prompt.
Ex :
Pour cela, éditez/créez le fichier ~/.bashrc
de votre utilisateur en y ajoutant cette ligne :
PS1='\[\e[33;01m\]\u \[\033[31;01m\][\[\033[0;37m\]\w\[\033[31;01m\]] \$\[\033[00m\] '
Dans cette ligne, les blocs de la forme \[\e[33;01m\]
, \[\033[31;01m\]
et \[\033[0;37m\]
définissent les
couleurs à utiliser : respectivement jaune, rouge et gris clair.
\u
affiche l'utilisateur et \w
le chemin courant. \$
affiche $ pour un utilisateur et # pour le root.
Il y a plein d'autres paramètres disponibles, pour afficher l'heure, le nom de la machine, ...
Modifié le :
Publié le :
Je viens de créer un nouveau skin dans JCMS, mais malgré un redémarrage ou le vidage du work de tomcat, il n'apparaît pas dans la liste de choix du skin de mes portlets.
Par défaut, pour ajouter un nouveau skin il suffit de créer la jsp et de la déclarer dans le fichier
plugin.xml
. Par exemple :
<types>
<templates type="AbstractPortletSkinable">
<template name="newSkin" file="doNewSkin.jsp" usage="box">
<label xml:lang="en">My new skin</label>
<label xml:lang="fr">Mon nouveau skin</label>
</template>
</templates>
</type>
Il existe cependant un option méconnue dans le CMS : la possibilité d'activer des skins différents pour chaque espace de travail.
Pour cela :
Par défaut dans JCMS, aucun n'est coché. Tous les nouveaux skins sont donc immédiatement disponibles.
Mais à partir du moment où l'un d'eux est coché, vous devrez repasser par cette interface à chaque création de skin pour l'activer manuellement.
Modifié le :
Publié le :
Par mesure de sécurité, JCMS limite le nombre de soumissions des formulaires. Il faut attendre environ une minute entre chaque soumission.
Ce comportement peut-être éviter en ajoutant cette classe css sur la balise form
du formulaire :
noSingleSubmitButton
.
Modifié le :
Publié le :
Lorsqu'on utilise des variables statiques, on peut souvent les initialiser directement à leur déclaration :
public class MaClasse {
public static String myStaticString = "Ma chaîne";
public static String[] myStaticArray = {"Ma chaîne 1", "Ma chaîne 2"};
}
Mais si vous avez une Collection
ou un autre objet en variable de classe, vous ne pourrez pas l'initialiser de cette manière.
Vous pouvez donc déclarer un bloc statique, qui sera exécuté une seule fois, à la première utilisation de la classe. Dans ce bloc, appelez une fonction statique qui se chargera d'initialiser votre variable :
public class MaClasse {
public static List<String> myStaticList = new ArrayList<String>();
static {
MaClasse.initMyStaticList();
}
private static void initMyStaticList() {
MaClasse.myStaticList.add("Ma chaîne 1");
MaClasse.myStaticList.add("Ma chaîne 2");
}
}
Modifié le :
Publié le :
Pour créer un lien symbolique sous Windows, il faut utiliser la commande symlink
:
mklink [[/D] | [/H] | [/J]] <Lien> <Cible>
Pour cela, lancez une invite de commande :
cmd
et validezSi par exemple vous souhaitez pouvoir accéder au répertoire C:\Windows\System32\drivers\etc
directement via C:\etc
, tapez cette commande :
mklink /J C:\etc C:\Windows\System32\drivers\etc
Remarques :
/D
et non pas /J
. Cela créera un lien symbolique et non pas une jonction.Modifié le :
Publié le :
Si vous souhaitez formater un numéro de téléphone, un IBAN, ou n'importe quelle chaîne en y ajoutant régulièrement un séparateur, cette fonction peut vous être utile :
/**
* Formate une chaîne en ajoutant un séparateur tous les <code>length</code> caractères.
*
* @param string Chaîne à formater
* @param separator Séparateur à ajouter
* @param length Taille des groupes de caractères à séparer
* @return La chaîne formatée
*/
public static String addChar(String string, String separator, int length) {
if (string != null && string.length() > length) {
string = string.replaceAll("([^\\s]{" + length + "})", "$0" + separator);
}
return string;
}
Modifié le :
Publié le :
Dans JCMS (7 et +) les erreurs sont affichées dans un bloc ressemblant à ça.
Vous devez tout d'abord transmettre votre message à JCMS.
// Ajoute un message d'erreur dans la request
setErrorMsg("Le téléchargement du document a échoué", request);
// Ajoute un message d'erreur dans la session
setErrorMsgSession("Le téléchargement du document a échoué", request);
// Ajoute un message d'erreur dans la request
JcmsContext.setErrorMsg("An error occured while saving your content", request);
// Ajoute un message d'erreur dans la session
JcmsContext.setErrorMsgSession("An error occured while saving your content", request);
Remarque :
Des méthodes équivalentes existent pour les messages d'information et d'avertissement.
Une fois le message transmis, il reste à l'afficher. Il suffit pour cela d'inclure ce bout de code :
<%@ include file='/jcore/doMessageBox.jsp' %>
Cette JSP fournie par JCMS, va récupérer tous les messages d'information, d'avertissement ou d'erreur présents en session, dans les paramètres de la request et dans le contexte de la page. Chaque message trouvé est affiché.
Remarque :
Depuis la version 7.1 JCMS utilise Bootstrap. Si vous êtes en version antérieure, le message d'erreur ne ressemblera pas à celui ci-dessus.
Modifié le :
Publié le :
Lors de l'affichage des champs en ExtraData, JCMS utilise l'ordre l'alphabétique des clés des propriétés d'ExtraData.
Si votre plugin.prop
contient les propriétés suivantes :
extra.Category.jcmsplugin.monplugin.premierchamp
extra.Category.jcmsplugin.monplugin.champ2
extra.Category.jcmsplugin.monplugin.champ3
extra.Category.jcmsplugin.monplugin.dernierchamp
Les champs s'afficheront dans cet ordre en back-office :
Modifié le :
Publié le :
DomPDF peut poser problème pour afficher les images et prendre en compte les feuilles de style.
A priori c'est parce que les URL vers ces fichiers sont relatives et commencent par un /
.
Pour éviter ce problème il suffit de modifier deux fichiers de l'API en supprimant
ce caractère au début des URL, avec ce code :
// Suppression du / devant l'url
$url = ltrim($url, '/');
Modifiez dompdf/include/dompdf.cls.php
:
protected function _process_html() {
$this->save_locale();
$this->_tree->build_tree();
$this->_css->load_css_file(Stylesheet::DEFAULT_STYLESHEET, Stylesheet::ORIG_UA);
$acceptedmedia = Stylesheet::$ACCEPTED_GENERIC_MEDIA_TYPES;
$acceptedmedia[] = $this->get_option("default_media_type");
// <base href="" />
$base_nodes = $this->_xml->getElementsByTagName("base");
if ( $base_nodes->length && ($href = $base_nodes->item(0)->getAttribute("href")) ) {
list($this->_protocol, $this->_base_host, $this->_base_path) = explode_url($href);
}
// Set the base path of the Stylesheet to that of the file being processed
$this->_css->set_protocol($this->_protocol);
$this->_css->set_host($this->_base_host);
$this->_css->set_base_path($this->_base_path);
// Get all the stylesheets so that they are processed in document order
$xpath = new DOMXPath($this->_xml);
$stylesheets = $xpath->query("//*[name() = 'link' or name() = 'style']");
foreach($stylesheets as $tag) {
switch (strtolower($tag->nodeName)) {
// load <link rel="STYLESHEET" ... /> tags
case "link":
if ( mb_strtolower(stripos($tag->getAttribute("rel"), "stylesheet") !== false) || // may be "appendix stylesheet"
mb_strtolower($tag->getAttribute("type")) === "text/css" ) {
//Check if the css file is for an accepted media type
//media not given then always valid
$formedialist = preg_split("/[\s\n,]/", $tag->getAttribute("media"),-1, PREG_SPLIT_NO_EMPTY);
if ( count($formedialist) > 0 ) {
$accept = false;
foreach ( $formedialist as $type ) {
if ( in_array(mb_strtolower(trim($type)), $acceptedmedia) ) {
$accept = true;
break;
}
}
if (!$accept) {
//found at least one mediatype, but none of the accepted ones
//Skip this css file.
continue;
}
}
$url = $tag->getAttribute("href");
// Suppression du / devant l'url
$url = ltrim($url, '/');
$url = build_url($this->_protocol, $this->_base_host, $this->_base_path, $url);
$this->_css->load_css_file($url, Stylesheet::ORIG_AUTHOR);
}
break;
Et dompdf/include/image_frame_decorator.cls.php
:
function __construct(Frame $frame, DOMPDF $dompdf) {
parent::__construct($frame, $dompdf);
$url = $frame->get_node()->getAttribute("src");
// Suppression du / devant l'url
$url = ltrim($url, '/');
$debug_png = $dompdf->get_option("debug_png");
if ($debug_png) print '[__construct '.$url.']';
list($this->_image_url, /*$type*/, $this->_image_msg) = Image_Cache::resolve_url(
$url,
$dompdf->get_protocol(),
$dompdf->get_host(),
$dompdf->get_base_path(),
$dompdf
);
if ( Image_Cache::is_broken($this->_image_url) &&
$alt = $frame->get_node()->getAttribute("alt") ) {
$style = $frame->get_style();
$style->width = (4/3)*Font_Metrics::get_text_width($alt, $style->font_family, $style->font_size, $style->word_spacing);
$style->height = Font_Metrics::get_font_height($style->font_family, $style->font_size);
}
}
Modifié le :
Publié le :
L'API JpGraph permet de créer tous les types de graphiques en PHP.
En quelques lignes de code, vous pouvez créer un camembert, un histogramme ou d'autres graphiques plus complexes.
Voici un aperçu de la galerie disponible sur le site :
Modifié le :
Publié le :
Voici une méthode pour tester si une chaîne est vide ou contient uniquement des espaces :
/**
* Retourne si la chaîne en argument est vide ou contient uniquement des espaces.
*
* @param string Chaîne
* @return <code>true</code> si la chaîne est nulle, vide, ou s'il ne contient que des caractères espaces (ex: \n \r \s, ...) y compris les espaces insécables.
*/
public static boolean isEmpty(String string) {
boolean isEmpty = true;
if (string != null) {
isEmpty = "".equals(string.replaceAll("[\\s|\\u00A0]+", ""));
}
return isEmpty;
}
Remarque :
\u00A0
représente les espaces insécables.
Si on utilise uniquement \s
dans le remplacement, ils ne seront pas considérés comme des espaces.
Modifié le :
Publié le :
Si vous utiliser Drush dans Drupal, vous pouvez activer automatiquement tous les modules déclarés en dépendance de votre profil.
Pour cela utilisez cette commande :
drush en $(grep dependencies /path/to/my-site/profiles/my_profile/my_profile.info | sed -n 's/dependencies\[\]=\(.*\)/\1/p')
Modifié le :
Publié le :
Il est possible d'effectuer des requêtes SQL sur une base de données autre que celle de Drupal,
tout en utilisant les fonctions db_select()
, db_query()
, ....
Pour cela, il faut déclarer la ou les bases externes dans le fichier site/default/settings.php
:
$databases = array (
'default' => array (
'default' => array (
'database' => 'drupal',
'username' => 'username',
'password' => 'password',
'host' => 'localhost',
'port' => '',
'driver' => 'mysql',
'prefix' => '',
),
),
'ma_nouvelle_base' => array (
'default' => array (
'database' => 'db1',
'username' => 'username2',
'password' => 'password2',
'host' => 'db.example.com',
'port' => '',
'driver' => 'mysql',
'prefix' => '',
),
),
);
Vous pouvez maintenant utiliser la nouvelle base dans vos modules, grâce à la fonction db_set_active()
:
// Sélection de la nouvelle base
db_set_active('ma_nouvelle_base');
// Exécution d'un requête
$results = db_query($sql);
// Retour à la base par défaut
db_set_active('default');
Modifié le :
Publié le :
L'application Java Decompiler permet de décompiler un fichier .class simple ou tous ceux présents dans un JAR.
Le site de l'application fournit un plugin Eclipse qui ne fonctionne pas avec les dernières versions (testé avec Eclipse Juno). Plusieurs fork ont été créés et ce site en propose un qui fonctionne : JDEclipse-Realign.
Pour l'installer :
Une fois installé et Eclipse redémarré, vérifiez l'association des fichiers :
Si vous ouvrez un fichier .class (via F3
ou Ctrl + clic
sur le nom d'une classe) vous devriez maintenant voir la source décompilée.
Modifié le :
Publié le :
Lorsque vous affichez un nœud dans le template node.tpl.php
ou une de ses surcharges
(ex: node--article.tpl.php
), vous avez souvent besoin d'effectuer des traitements particuliers.
Pour séparer la partie traitement de l'affichage, il est préférable de mettre le maximum de code
PHP dans votre fichier mymodule.module
(ou mieux, dans d'autres fichiers PHP à vous).
Pour cela, utilisez le hook_node_view()
.
Par exemple si dans le template d'un article on veut afficher les 3 derniers articles publiés :
/**
* Implements hook_node_view().
*/
function mymodule_node_view($node, $view_mode, $langcode) {
global $language;
if ($node->type === 'article) {
// Last published articles
$query = db_select('node', 'n')
->fields('n', array('nid'))
->condition('status', 1)
->condition('bundle', 'article')
->orderBy('changed', 'DESC')
->range(0, 3);
$nids = $query->execute()->fetchCol();
$nodes = !empty($nids) ? node_load_multiple($nids) : array();
$node->content['last_published_articles'] = $nodes;
}
}
Explications :
$content['last_published_articles']
sera disponible dans le template.Remarque :
$view_mode
, pour éviter d'effectuer le traitement là où c'est inutile.Modifié le :
Publié le :
Pour créer un bloc programmatiquement, vous devez déjà avoir créé un module.
Deux hooks vont être nécessaires dans le fichier mymodule.module
.
L'un pour déclarer votre bloc, l'autre pour définir son contenu.
/**
* Implements hook_block_info();
*/
function mymodule_block_info() {
$blocks['myblock'] = array(
'info' => t('My block'),
'cache' => DRUPAL_CACHE_PER_ROLE,
);
$blocks['myotherblock'] = array(
'info' => t('My other block'),
'cache' => DRUPAL_CACHE_PER_PAGE,
);
return $blocks;
}
Explications :
/**
* Implements hook_block_view();
*/
function mymodule_block_view($delta = '') {
switch ($delta) {
case 'myblock' :
$block['subject'] = t('My block');
$block['content'] = '<p>Contenu de mon bloc.</p>';
break;
case 'myotherblock' :
$block['content'] = 'Contenu de mon second bloc.';
break;
}
return $block;
}
Explications :
content
, avec du texte simple ou du html en valeur.Remarque :
Si vous affichez des nœuds dans votre bloc, vous pourrez appeler directement la fonction node_view()
pour générer leur contenu html. Ex :
$content = '<h2>' . t('Last published') . '</h2>';
$content .= '<ul>';
foreach ($node_list as $node) {
$content .= '<li>';
$content .= node_view($node, 'list');
$content .= '</li>';
}
$content .= '</ul>';
$block['content'] = $content;
Modifié le :
Publié le :
La création d'un module est très rapide dans Drupal. Dans cet exemple, on créera le module mymodule
.
Commencez par créer le répertoire mymodule/
. Vous pouvez le placer directement
dans site/all/modules
, ou créer un répertoire intermédiaire qui regroupera tous vos modules
(ex: site/all/modules/myproject/mymodule/
, ou site/all/modules/custom/mymodule/
).
Basiquement, un module n'a besoin que de deux fichiers, tous deux à la racine du répertoire :
mymodule.info
et mymodule.module
.
Le fichier .info
permet de décrire votre module.
name = mymodule
description = module de test
package = Mypackage
core = 7.x
version = "7.x-1.0"
Explications :
Le fichier .module
contiendra une bonne partie de votre code PHP, et surtout vos hooks.
Pour l'instant, créez un fichier mymodule.module
vide.
Une fois fait, et après vidage des caches, vous devriez voir votre module en back-office :
Modifié le :
Publié le :
Pour créer un mode d'affichage programmatiquement, vous devez déjà avoir créé un module.
Par défaut, Drupal propose les modes d'affichage suivant :
Contenu complet (= Full), Accroche (= Teaser) et RSS.
Le hook_entity_info_alter()
dans le fichier mymodule.module
permet d'en ajouter de nouveaux.
/**
* Implements hook_entity_info_alter();
*/
function mymodule_entity_info_alter(&$entity_info) {
$entity_info['node']['view modes']['my_view_mode'] = array(
'label' => t('My view mode'),
'custom settings' => FALSE,
);
}
Après avoir vidé les caches, vous devriez voir votre nouveau mode d'affichage en back-office :
Modifié le :
Publié le :
Description :
Traduit une chaîne de caractères, avec d'éventuels paramètres. Le troisième argument permet de spécifier un contexte ou une langue.
Exemple :
$text = t('String with @myparam', array('@myparam' => $my_value));
Description :
Génère L’URL vers le thumbnail d'image correspondant au style d'image en premier argument.
Exemple :
image_style_url(
'image_thumbnail',
$node->field_photos[$language->language][$index]['uri']
);
Description :
Crée une balise <a>
avec le premier argument comme libellé et le deuxième en href
.
Exemple :
image_style_url(
'image_thumbnail',
$node->field_photos[$language->language][$index]['uri']
);
Description :
Génère l'URL vers un fichier ou une image, à partir de son uri (de la forme public://mon-image-03.jpg
).
Exemple :
file_create_url($node->field_logo[$language->language][0]['uri']);
Description :
Génère une URL interne ou externe, à partir d'une URL relative ou absolue.
Exemple :
url('node/' . $node->nid)
Description :
Retourne l'alias d'URL pour la page avec le chemin en argument
Exemple :
drupal_get_path_alias('node/' . $node->nid)
Description :
Retourne le tableau en entrée, sans les valeurs dont la clé commence par #
.
Description :
Affiche le contenu de la variable, ses éléments dans le cas d'un tableau, ses attributs dans le cas d'un objet, de manière récursive. (Cette fonction est fournie par le module devel)
Description :
Affiche la requête finale. (Cette fonction est fournie par le module devel)
Modifié le :
Publié le :
Pour effectuer des traitements en masse et limiter les problèmes de mémoire, on peut demander à Drupal de les gérer par lots.
Supposons par exemple que l'on souhaite supprimer tous les nœuds de type article, et qu'il y en ait une dizaine de milliers.
// Récupération des nid des nœuds de type article
$results = db_select('node', 'n')
->fields('n', array('nid'))
->condition('type', 'article', '=')
->execute()
->fetchCol();
Pour éviter de tout supprimer en une fois, on peut effectuer des suppressions par lots de 500 nœuds.
$nb_op = 500;
$nb_total = count($results);
// Découpage des traitements en lots
foreach (array_chunk($results, $nb_op) as $nids) {
$operations[] = array('_my_module_batch_delete_nodes', array($nids, $nb_total));
}
// Construction du tableau de paramètre pour le batch
$batch = array(
'operations' => $operations,
'title' => t('Delete batch'),
'init_message' => t('Initializing'),
'error_message' => t('An error occurred'),
'finished' => 'my_module_my_custom_end_function'
);
// Exécution du batch
batch_set($batch);
drush_backend_batch_process();
La fonction qui va effectuer la suppression est _my_module_batch_delete_nodes()
:
/**
* Custom batch function to delete multiple nodes.
*
* @param $nids Nids of nodes that must be deleted
* @param $nb_total Number of nodes already deleted
* @param $context Context to display the progression
*/
function _my_module_batch_delete_nodes($nids, $nb_total, &$context) {
if (empty($context['results']['progress_d'])) {
$context['results']['progress_d'] = 0;
}
node_delete_multiple($nids);
// Affichage de la progression
$context['results']['progress_d'] += count($nids);
$context['message'] = 'Deleted ' . $context['results']['progress_d'] . '/' . $total;
}
Modifié le :
Publié le :
Voici une classe utilitaire pour géocoder une adresse et/ou récupérer les limites (= bounds) d'une ville. Elle utilise l'API Google Map.
/**
* Classe utilitaire de géocodage.
*
* Permet de :
* - géocoder simplement une adresse auprès de l'API Google Map.
* - récupérer les limites d'une ville.
*
* L'appel à l'API Google Map utilise curl.
*
* @class Geocoder
*/
class Geocoder {
/**
* L'URL d'appel au service de geocodage de Google.
*
* @const string
*/
const BASE_URL = 'https://maps.google.com/maps/api/geocode/json';
/**
* Clé d'API Google Map.
*
* @var string
*/
private $apiKey;
/**
* ID client à utiliser avec la clé privée ci-dessous.
*
* @var string
*/
private $clientID;
/**
* Clé privée permettant de dépasser la limitation de 2000 requêtes/jour.
*
* @var string
*/
private $privateKey;
/**
* Copnstructeur.
*
* @param string $api_key
* Clé d'API Google Map.
* @param string $client_id
* ID client pour l'API. Laisser vide pour une utilisation classique.
* @param string $private_key
* Clé secrète associéeà 'ID client. Laisser vide pour
* une utilisation classique.
*/
public function __construct($api_key, $client_id = '', $private_key = '') {
$this->apiKey = $api_key;
$this->clientID = $client_id;
$this->privateKey = $private_key;
}
/**
* Retourne le nom d'une ville à partir du tableau de résultat de géocodage.
*
* @param array $geocode_result
* Tableau de résultat d'un géocodage.
*
* @return string
* Un tableau dont les clés sont 'top', 'right, 'bottom' et 'left'
*/
public function getName(array $geocode_result) {
$city_name = '';
$address_components = $geocode_result['address_components'];
$i = 0;
$length = count($address_components);
while (empty($city_name) && $i < $length) {
if (!empty($address_components[$i]['types'][0]) && $address_components[$i]['types'][0] == 'locality') {
$city_name = $geocode_result['address_components'][$i]['long_name'];
}
$i++;
}
return $city_name;
}
/**
* Retourne les limites d'une ville.
*
* @param array $geocode_result
* Tableau de résultat d'un géocodage.
*
* @return array
* Un tableau dont les clés sont 'top', 'right, 'bottom' et 'left'
*/
public function getBounds(array $geocode_result) {
$bounds = [];
if (!empty($geocode_result['geometry']['bounds'])) {
$geo_bounds = $geocode_result['geometry']['bounds'];
$bounds = [
'top' => $geo_bounds['northeast']['lat'],
'right' => $geo_bounds['northeast']['lng'],
'bottom' => $geo_bounds['southwest']['lat'],
'left' => $geo_bounds['southwest']['lng'],
];
}
return $bounds;
}
/**
* Retourne le nom de la ville et ses limites.
*
* @param string $city_name
* Nom de la ville, avec son code postal.
*
* @return array
* Un tableau dont les clés sont 'name' et 'bounds'
*/
public function getCity($city_name) {
$city = NULL;
$geocode = $this->geocode($city_name);
if (!empty($geocode)) {
$city['name'] = $this->getName($geocode);
$city['bounds'] = $this->getBounds($geocode);
}
return $city;
}
/**
* Retourne le résultat d'un géocodage sur l'adresse en argument.
*
* @param string $address
* Adresse à géocoder.
*
* @return array
* Le flux json de Google décodé
*/
public function geocode($address) {
$result = NULL;
$query_parameters = [
'address' => $address,
'key' => $this->apiKey,
'result_type' => 'locality',
'components' => 'country:FR',
'sensor' => 'false',
];
$url = self::BASE_URL;
$query_string = '';
foreach ($query_parameters as $key => $value) {
$query_string .= '&' . $key . '=' . urlencode($value);
}
$url .= '?' . substr($query_string, 1);
if (!empty($this->clientID) && !empty($this->private_key)) {
$url = $this->signUrl($url);
}
$json_response = $this->curlFileGetContent($url);
$response = json_decode($json_response, TRUE);
if ($response['status'] == 'OK') {
$result = $response['results'][0];
}
return $result;
}
/**
* Retourne l'URL en argument en y ajoutant le paramètre de signature.
*
* Ce paramètre de signature est construit à partir de la clé privée et de
* l'ID client. Elle permet notamment de dépasser les 2000 requêtes/jour.
*
* @param string $unsigned_url
* URL à signer.
*
* @return string
* L'URL signée
*/
private function signUrl($unsigned_url) {
$url = parse_url($unsigned_url);
$url_part_to_sign = $url['path'] . "?" . $url['query'];
if (strpos($url_part_to_sign, 'client') === FALSE) {
$url_part_to_sign .= '&client=' . $this->clientID;
$unsigned_url .= '&client=' . $this->clientID;
}
// Décode la clé privée dans son format binaire
$decoded_key = $this->decodeBase64UrlSafe($this->privateKey);
// Crée une signature binaire via HMAC SHA1
$signature = hash_hmac('sha1', $url_part_to_sign, $decoded_key, TRUE);
$encoded_signature = $this->encodeBase64UrlSafe($signature);
return $unsigned_url . '&signature=' . $encoded_signature;
}
/**
* Appelle l'URL en argument via curl, et retourne le résultat de l'exécution.
*
* @param string $url
* URL à appeler.
*
* @return mixed
* Le retour de l'API
*/
private function curlFileGetContent($url) {
$c = curl_init();
curl_setopt($c, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($c, CURLOPT_URL, $url);
$content = curl_exec($c);
curl_close($c);
return $content;
}
/**
* Encode une chaîne en base 64.
*
* Échappe les caractères gênants dans une URL (+ => - et / => _).
*
* @param string $value
* Chaîne à encoder.
*
* @return string
* La chaîne encodée
*/
private function encodeBase64UrlSafe($value) {
$base64_value = base64_encode($value);
return str_replace(['+', '/'], ['-', '_'], $base64_value);
}
/**
* Décode une chaîne en base 64 dont les caractères gênants sont échappés.
*
* @param string $value
* Chaîne à décoder.
*
* @return string
* La chaîne décodée
*/
private function decodeBase64UrlSafe($value) {
$value = str_replace(['-', '_'], ['+', '/'], $value);
return base64_decode($value);
}
}
Pour récupérer les limites de la ville de Paris, on aura par exemple :
$api_key = '<MON API KEY>';
$city = 'Paris';
$postal_code = 75000;
$geocoder = new Geocoder($api_key);
$geocoder->getCity($city . ' ' . $postal_code);
Remarques :
Modifié le :
Publié le :
Drupal fournit 3 hooks pour effectuer des traitements après création/édition/suppression de contenu.
/**
* Implements hook_node_insert().
*/
function my_module_node_insert($node) {
// Do something
}
/**
* Implements hook_node_update().
*/
function my_module_node_update($node) {
// Do something
}
/**
* Implements hook_node_delete().
*/
function my_module_node_delete($node) {
// Do something
}
Dans le cadre de contenus (et autres entités) multilingues, 3 autres hooks permettent de connaitre la langue pour laquelle la révision est créée/supprimmée.
La langue utilisée est disponible dans le hook vie la variable $translation['language']
.
Le type d'entité (ex: user, node, ...) est disponible dans la variable $entity_type
.
/**
* Implements hook_entity_translation_insert().
*/
function my_module_entity_translation_insert($entity_type, $entity, $translation, $values = array()) {
// Do something
}
/**
* Implements hook_entity_translation_update().
*/
function my_module_entity_translation_update($entity_type, $entity, $translation, $values = array()) {
// Do something
}
/**
* Implements hook_entity_translation_delete().
*/
function my_module_entity_translation_delete($entity_type, $entity, $langcode) {
// Do something
}
Modifié le :
Publié le :
Lorsqu'on crée programmatiquement un contenu,
voici comment remplir différents types de champ
(les lignes ci-dessous sont à ajouter à la place de [...(1)]
et/ou [...(2)]
dans l'article lié).
Dans Drupal, tous les champs sont potentiellement multivalués.
Pour ajouter plusieurs valeurs à un même champ, il suffit d'ajouter une nouvelle ligne en
incrémentant le [0]
dans les lignes ci-dessous.
Par exemple pour un champ texte basique ayant 3 valeurs :
$node->field_text[LANGUAGE_NONE][0]['value'] = 'ma première valeur';
$node->field_text[LANGUAGE_NONE][1]['value'] = 'ma deuxième valeur';
$node->field_text[LANGUAGE_NONE][2]['value'] = 'ma troisième valeur';
Dans Drupal, tous les champs sont potentiellement multilingues.
Pour ajouter une valeur dans une autre langue, il suffit d'ajouter une ligne en remplaçant
LANGUAGE_NONE
par une langue.
Exemple avec un champ texte basique et deux langues :
$node->field_text['fr'][0]['value'] = 'ma valeur';
$node->field_text['en'][1]['value'] = 'my value';
Pour remplir un champ texte basique, cette ligne suffit :
$node->field_text[LANGUAGE_NONE][0]['value'] = 'ma valeur';
Si ce champ utilise un format de texte particulier, il faut le préciser. Exemple avec simple_text
:
$node->field_text[LANGUAGE_NONE][0]['value'] = 'ma valeur';
$node->field_text[LANGUAGE_NONE][0]['format'] = 'simple_text';
Même principe pour les champs de type nombre :
$node->field_number[LANGUAGE_NONE][0]['value'] = 42;
Encore la même chose pour les booléens, $my_boolean
étant en fait un entier égal à 0 ou 1 :
$my_boolean = 0;
$node->field_number[LANGUAGE_NONE][0]['value'] = $my_boolean;
Pour importer une image programmatiquement vous pouvez utiliser cette fonction :
/**
* Copy the image in argument in the drupal upload dir, and return it.
*
* @param string $image_path Image path from the root directory
* @return array an array representing the copied image
*/
private function copy_image($image_path) {
$root_dir_path = getcwd();
$upload_sample_files_uri = file_default_scheme() . '://sample_data';
$file_path = $root_dir_path . $image_path;
$file = (object) array(
'uid' => 1,
'uri' => $file_path,
'filemime' => file_get_mimetype($file_path),
'status' => 1,
);
$file = file_copy($file, $upload_sample_files_uri);
return (array) $file;
}
Pour une image dans un répertoire temp/
à la racine de Drupal ça donne ça :
$node->field_image[LANGUAGE_NONE][0] = copy_image('/temp/mon_image.jpg');
Un champ lien avec une URL, un libellé et d'éventuels attributs HTML :
$node->field_link[LANGUAGE_NONE][0] = array(
'url' => 'http://www.google.fr',
'title' => 'Libellé du lien',
'attributes' => array('title' => 'Contenu de l'attribut HTML title'),
);
Le champ entityref stocke des id (et donc des nid pour des nœuds) :
$node->field_related_content[LANGUAGE_NONE][0]['target_id'] = $other_node->nid;
Le champ adresse découpe les adresses en 5 parties :
$node->field_adresse[LANGUAGE_NONE][0] = array(
'country' => 'FR',
'locality' => 'Paris',
'postal_code' => '75000',
'thoroughfare' => '1, Avenue des Champs Élysées',
'premise' => '2ème étage',
);
Le champ coordonnées permet entre autres de stocker des limites géographiques : des bounds.
$node->field_coordonnees[LANGUAGE_NONE][0]['input_format'] = GEOFIELD_INPUT_BOUNDS;
$node->field_coordonnees[LANGUAGE_NONE][0]['geom']['left'] = '2.320915';
$node->field_coordonnees[LANGUAGE_NONE][0]['geom']['top'] = '48.869911';
$node->field_coordonnees[LANGUAGE_NONE][0]['geom']['right'] = '2.350928';
$node->field_coordonnees[LANGUAGE_NONE][0]['geom']['bottom'] = '48.854086';
Le champ métadonnées ajoutera des balises méta dans le <head>
de la page :
$node->metatags[LANGUAGE_NONE] = array(
'title' => array('value' => 'Contenu de la balise title de la page'),
'description' => array('value' => 'Contenu de la balise méta description'),
'abstract' => array('value' => 'Contenu de la balise méta abstract'),
'keywords' => array('value' => 'Contenu de la balise méta keywords'),
);
D'autres clés peuvent être ajoutées au tableau.
Modifié le :
Publié le :
La fonction orderRandom()
permet de trier les résultats d'une requête de manière aléatoire :
db_select('node', 'n')
->fields('n', array('nid'))
->condition('status', 1)
->range(0, 10)
->orderRandom();
La requête ci-dessus retourne 10 nœuds aléatoires, à l'état publié.
Modifié le :
Publié le :
Pour rediriger l'utilisateur après sa connexion, on peut utiliser le hook_user_login()
:
/**
* Implements hook_user_login().
*/
function my_module_user_login(&$edit, $account) {
if (!isset($_POST['form_id']) || $_POST['form_id'] != 'user_pass_reset') {
if (in_array('authenticated user', $account->roles)) {
// Modification de l'url de destination
$_GET['destination'] = 'admin/workbench/content/all';
}
}
}
L'exemple ci-dessus redirige l'utilisateur vers la page de gestion des contenus
du module workbench (admin/workbench/content/all
).
Modifié le :
Publié le :
/**
* Retourne une publication du type en paramètre en remplissant automatiquement ses champs, et après l'avoir créer dans JCMS.
*
* @param clazz La classe du type de contenu souhaité
* @return une publication du type souhaité
*/
public static <T extends Publication> T fillPublication(Class<T> clazz) {
Publication publication = null;
if (Util.notEmpty(clazz)) {
try {
publication = clazz.newInstance();
} catch (InstantiationException e) {
LOGGER.error("Impossible d'instancier dynamiquement un objet de la classe \"" + clazz.getSimpleName() + "\"", e);
} catch (IllegalAccessException e) {
LOGGER.error("Impossible d'instancier dynamiquement un objet de la classe \"" + clazz.getSimpleName() + "\"", e);
}
}
return clazz.cast(publication);
}
Publication publication = new Article();
Class<? extends Publication> clazz;
Method method = clazz.getMethod("getName");
String name= (String) method.invoke(publication, null);
L'intérêt de ce code est d'appeler une méthode de l'objet publication sans savoir que c'est un article.
int size = 3;
Objet[] publicationArray = (Publication[]) java.lang.reflect.Array.newInstance(Publication.class, size);
Modifié le :
Publié le :
Si dans un formulaire vous avez une checkbox et que vous voulez savoir si elle est cochée, vous pouvez utiliser la fonction is() de jQuery :
jQuery('#ma_checkbox_id').is(':checked')
Par exemple, pour loguer un message lorsqu'on coche/décoche une checkbox :
jQuery('#ma_checkbox_id').on('change', function() {
// Si la checkbox est cochée
if (jQuery(this).is(':checked')) {
console.log('Je viens d\'être cochée !');
} else {
console.log('Je viens d\'être décochée !');
}
});
Modifié le :
Publié le :
Si vous utilisez le SDK Facebook pour PHP en suivant l'exemple
fourni avec les sources vous aurez peut-être ce problème :
la méthode getUser()
qui retourne toujours 0.
De nombreuses personnes ont eu ce problème (cf. recherche Google, pour des raisons diverses et variées.
Voici une explication possible :
le SDK ne trouve pas le certificat fourni avec les sources (fb_ca_chain_bundle.crt
).
Pour savoir si vous avez cette erreur, consultez les logs d'Apache et cherchez le message :
Invalid or no certificate authority found, using bundled information.
Si vous le trouvez, c'est que le certificat ne se trouve pas au niveau de votre script.
Pour spécifier le bon chemin vers le fichier, utilisez cette ligne de code :
\Facebook::$CURL_OPTS[CURLOPT_CAINFO] = getcwd() . '/path_depuis_le_repertoire_courant/fb_ca_chain_bundle.crt';
Modifié le :
Publié le :
Le cœur d'eZ Publish 5 se trouve dans le répertoire vendor/ezsystems/ezpublish-kernel/
de l'application.
Voici quelques répertoires utiles qu'il contient :
eZ/Publish/API/Repository/
: contient les interfaces des services avec les signatures de toutes leurs méthodes.eZ/Publish/Core/
: contient l'implémentation des interfaces du répertoire précédent
Repository/
: contient l'implémentation de ces mêmes services.Base/Exceptions/
: contient toutes les exceptions fournies par eZ, et surtout leurs constructeurs.Persistence/Legacy/Content/Search/Gateway/CriterionHandler/
:
contient les critères de recherche fournis par eZ.Persistence/Legacy/Content/Search/Gateway/SortClauseHandler/
:
contient les méthodes de tri fournies par eZ.Remarque :
Les 4 derniers répertoires, sont sous eZ/Publish/Core/
.
Modifié le :
Publié le :
L'extension eZTags pour eZ Publish fournit un système de mots clés pour regrouper des contenus par thématique. Avec elle arrive un nouveau type de champ, pour taguer vos contenus.
Dans eZ Publish 4.x (ou en mode legacy), le template de ce champ affiche un lien vers une page qui liste les contenus avec ce mot clé. La version pour eZ Publish 5 est disponible ici. Malheureusement, elle ne fournit aucune méthode pour trouver des contenus à partir d'un mot clé.
Voici trois méthodes pour récupérer ces contenus.
<?php
// [...]
use \Netgen\TagsBundle\API\Repository\Values\Tags\Tag;
/**
* Retrouve les contenus avec le mot clé en argument.
*
* @param string $keyword Mot clé recherché
* @param int $offset Offset pour les résultats de la recherche
* @param int $limit Nombre maximal de résultats de recherche
*
* @return \eZ\Publish\API\Repository\Values\Content\Content[]
*
* @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException Si l'utilisateur courant n'a pas le droit de voir les tags
* @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException Si aucun tag avec cet ID n'existe
*/
public function getRelatedContentByKeyword($keyword, $offset = 0, $limit = 50) {
$rootTagID = 2;
// Recherche du tag correspond au mot clé
$tag = $this->getTagByKeyword($rootTagID, $keyword);
$relatedContentList = array();
if (!empty($tag)) {
// Recherche des contenus avec le mot clé souhaité
$tagService = $this->container->get('ezpublish.api.service.tags');
$relatedContentList = $tagService->getRelatedContent($tag, $offset, $limit);
}
return $relatedContentList;
}
/**
* Retrouve un Tag à partir de son mot clé.
* Le premier trouvé parmi les descendants de celui dont l'ID est en argument est retourné.
*
* @param string $rootTagID ID du tag parmi les descendants duquel rechercher
* @param string $keyword Mot clé recherché
*
* @return \Netgen\TagsBundle\API\Repository\Values\Tags\Tag
*
* @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException Si l'utilisateur courant n'a pas le droit de voir le tag courant
* @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException Si aucun tag avec cet ID n'existe
*/
public function getTagByKeyword($rootTagID, $keyword) {
$tag = null;
// Récupération du tag racine
$tagService = $this->container->get('ezpublish.api.service.tags');
$rootTag = $tagService->loadTag($rootTagID);
if (!empty($rootTag)) {
// Récupération des tags descendants
$descendantTagList = $this->getTagDescendantList($rootTag);
if (!empty($descendantTagList)) {
// Parcours des tags descendants
for ($i = 0, $length = count($descendantTagList); $i < $length && $tag == null; $i++) {
if ($descendantTagList[$i]->keyword == $keyword) {
$tag = $descendantTagList[$i];
}
}
}
}
return $tag;
}
/**
* Retourne tous les tags descendant de celui en argument.
*
* @param \Netgen\TagsBundle\API\Repository\Values\Tags\Tag $rootTag Tag racine
*
* @return \Netgen\TagsBundle\API\Repository\Values\Tags\Tag[]
*
* @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException Si l'utilisateur courant n'a pas le droit de voir le tag courant
*/
public function getTagDescendantList( Tag $rootTag ) {
// Récupération des tag descendants
$descendantTagList = array();
$tagService = $this->container->get( 'ezpublish.api.service.tags' );
$childrenTagList = $tagService->loadTagChildren( $rootTag );
// Parcours des tags enfants
foreach ( $childrenTagList as $childTag ) {
$descendantTagList[] = $childTag;
// Récupération des descendants
$descendantTagList = array_merge( $descendantTagList, $this->getTagDescendantList( $childTag ) );
}
return $descendantTagList;
}
Remarques :
$container
(ContainerInterface
) est disponible.$rootTagID
(identifiant du tag racine) est en dur et devrait
être récupérée depuis un fichier de configuration.Modifié le :
Publié le :
Twig fournit de nombreuses fonctions et une liste de filtres pour simplifier le développement des templates.
Quelques exemples :
{# Des fonctions natives : #}
Contenu d'une variable : {{ dump(my_var) }}
Nombre aléatoire : {{ random(5) }}
{# Des filtres natifs : #}
Taille d'un tableau : {{ my_array|length }}
Mise en minuscule : {{ my_string|upper }}
Échappement de caractère : {{my_string|escape}}
L'intérêt de Twig c'est qu'il est très facilement extensible, et vous vous pouvez créer vos propres fonctions et vos propres filtres. Par exemple :
{# Une nouvelle fonction : #}
Affiche l'Url actuelle : {{ current_uri() }}
{# Un nouveau filtre : #}
{{ "Ma phrase est trop longue parce que la fin n'est pas intéressante."|truncate(28) }}
Acme/MyBundle
,
et l'avez activé dans le fichier ezpublish/EzPublishKernel.php
.Remarque :
Si ce n'est le nom du fichier de kernel, tout cet exemple est valable pour une application Symfony 2 non eZ.
L'ajout de filtres et fonctions se fait via un fichier PHP, qu'on appelle une extension Twig.
Créez le répertoire Twig/
dans votre bundle, et le fichier MyExtension.php
à l'intérieur :
<?php
namespace AT\APIToolsBundle\Twig;
use \Symfony\Component\DependencyInjection\ContainerInterface;
class MyExtension extends \Twig_Extension {
/**
* @var \Symfony\Component\DependencyInjection\ContainerInterface;
*/
protected $container;
/**
* Contructeur de l'extension Twig MyExtension.
*
* @param \Symfony\Component\DependencyInjection\ContainerInterface $container
*/
public function __construct(ContainerInterface $container) {
$this->container = $container;
}
/**
* Retourne le nom de l'extension.
*
* @return string
*/
public function getName() {
return 'MyExtension';
}
/**
* Retourne la liste des Filtres de template à ajouter à Twig.
*
* @return array
*/
public function getFilters() {
return [
'truncate' => new \Twig_Filter_Method($this, 'truncate'),
];
}
/**
* Retourne la liste des Fonctions de template à ajouter à Twig.
*
* @return array
*/
public function getFunctions() {
return [
'current_uri' => new \Twig_Function_Method($this, 'getCurrentURI'),
];
}
/**
* Retourne l'URI courante.
*
* @return string $_SERVER['REQUEST_URI']
*/
public function getCurrentURI() {
return $_SERVER['REQUEST_URI'];
}
/**
* Tronque le texte en argument.
* Si la longueur du texte est supérieure à $maxLength, $suffix est ajouté à la chaîne.
*
* @param string $text Chaîne à tronquer
* @param int $maxLength Longueur maximale autorisée pour la chaîne
* @param string $suffix Le sufixe à ajouter si besoin
* @return string
*/
public function truncate($text, $maxLength, $suffix = '...') {
$truncatedText = $text;
mb_internal_encoding('UTF-8');
$length = mb_strlen($text );
$sufixlength = mb_strlen($suffix);
// Si le texte est trop long
if ($length > $maxLength && $length >= $sufixlength) {
$truncatedText = mb_substr($text, 0, $maxLength - $sufixlength) . $suffix;
}
return $truncatedText;
}
}
Explications :
MyExtension
étend la classe Twig_Extension
fournie par Symfony.getName()
retourne le nom de votre choix pour votre extension.getFilters()
et getFunctions()
retournent la liste des filtres et
des fonctions à ajouter à Twig.truncate
et current_uri
).new \Twig_Filter_Method($this, 'méthode_à_appeler')
.new \Twig_Function_Method($this, 'méthode_à_appeler')
.L'extension Twig est terminée mais Symfony ne sait pas encore qu'elle existe.
Il vous faut la déclarer en tant que service, dans le fichier Resources/config/services.yml
de votre bundle :
parameters:
acme_my.twig_extension.class: Acme\MyBundle\Twig\MyExtension
services:
acme_my.twig_extension:
class: %acme_my.twig_extension.class%
arguments: [@service_container]
tags:
- { name: twig.extension }
Explications :
acme_my.twig_extension
).MyExtension
comme paramètre. Si on déplace ou renomme la classe par la suite,
seul le paramètre sera à changer.twig.extension
pour que Symfony sache de quel type de service il s'agit.Modifié le :
Publié le :
Le but de cet article est de proposer un exemple de page de connexion. Il s'articule autour de deux fichiers principaux : un template Twig et un contrôleur PHP.
Il est réalisé entièrement en mode Symfony, sans utiliser le stack Legacy.
Si l'utilisateur saisit de mauvais identifiants, un message d'erreur est affiché. Une fois connecté, il est redirigé vers la page qu'il consultait avant de se connecter.
Acme/MyBundle
, et l'avez activé dans le fichier
ezpublish/EzPublishKernel.php
.AcmeMyBundle::pagelayout.html.twig
.
Il possède un bloc nommé col_main
.redirect_uri
doit être définie et contenir l'url courante
(ex: /Ma-rubrique/Mon-article
).Dans le répertoire de templates de votre bundle (Resources/views/
),
créez les répertoires user/connection/
, qui contiendront tous les templates pour la connexion des utilisateurs
(connexion, inscription, ...)
Créez ensuite le fichier login.html.twig
dans le répertoire user/connection/
.
Voici à quoi il peut ressembler :
{# Surcharge du bloc 'col_main' du template pagelayout.html.twig, pour la page de connexion #}
{% extends noLayout ? viewbaseLayout : "AcmeMyBundle::pagelayout.html.twig" %}
{#
Affiche un article en mode Full
Paramètres :
- noLayout : False
- fail_login : Si la connexion a échouée
- redirect_uri : URI vers laquelle rediriger après la connexion
#}
{% block col_main %}
<div class="main-content user user-login panel">
{# Titre #}
<header class="heading">
<h1>Connexion</h1>
</header>
{# Contenu #}
<div class="content-body">
{# Message d'erreur #}
{% if fail_login %}
<div class="alert alert-danger">
<button data-dismiss="alert" class="close" type="button">×</button>
<p><strong>Erreur !</strong></p>
<p>Identifiant ou mot de passe invalide.</p>
</div>
{% endif %}
{# Formulaire #}
<form class="form-horizontal" method="post"
action="{{ path( 'acme_my_user_login', { 'redirectURI': redirect_uri } ) }}">
<div class="form-group {% if fail_login %}has-error{% endif %}">
<label class="col-lg-3 control-label" for="login">Identifiant</label>
<div class="col-lg-4">
<input type="text" placeholder="Identifiant"
id="login" name="Login" class="form-control input-small">
</div>
<div class="validation col-lg-5">
<img src="{{ asset( "bundles/acmemy/images/error.png" ) }}" alt="Erreur" />
</div>
</div>
<div class="form-group {% if fail_login %}has-error{% endif %}">
<label class="col-lg-3 control-label" for="password">Mot de passe</label>
<div class="col-lg-4">
<input type="password" placeholder="Mot de passe"
id="password" name="Password" class="form-control input-small">
</div>
<div class="validation col-lg-5">
<img src="{{ asset( "bundles/acmemy/images/error.png" ) }}" alt="Erreur" />
</div>
</div>
<div class="form-group">
<div class="col-lg-10 text-right">
<button class="btn btn-primary" type="submit">Connexion</button>
</div>
</div>
</form>
</div>
</div>
{% endblock %}
Explications :
col_main
.{% if fail_login %}
).acme_my_user_login
, sur laquelle Symfony va brancher le futur contrôleur.
L'URL courante lui sera transmise en argument.Remarques :
Resources/public/images/
du bundle.{% trans %}
et {% endtrans %}
, pour pouvoir être traduits
facilement par la suite si besoin.C'est le contrôleur qui va gérer les actions de connexion et déconnexion des utilisateurs. Il aura donc deux actions : login et logout.
Créez le fichier UserConnectionController.php
dans le répertoire Controller/
de votre Bundle :
<?php
namespace Acme\MyBundle\Controller;
use \eZ\Bundle\EzPublishCoreBundle\Controller;
use \eZ\Publish\API\Repository\Exceptions\NotFoundException;
use \eZ\Publish\API\Repository\Values\User\User;
use \Symfony\Component\HttpFoundation\Cookie;
use \Symfony\Component\HttpFoundation\RedirectResponse;
/**
* Controleur pour gérer les vues de connexion des utilisateurs.
*/
class UserConnectionController extends Controller {
/**
* Gestion de l'affichage de la page de login.
* - Affiche la page de login par défaut
* - Affiche d'éventuelles erreur de connexion
* - Connecte et redirige l'utilisateur
*
* @param string URI vers laquelle rediriger après la déconnexion
*
* @return \Symfony\Component\HttpFoundation\Response
*/
public function loginAction( $redirectURI ) {
$failLogin = false;
// Suppression d'un éventuel doublon dans l'URI
if ( ( $offset = strpos( $redirectURI, '/user/login' ) ) === 0 ) {
$redirectURI = substr( $redirectURI, $offset );
}
$request = $this->getRequest()->request;
// Si le formulaire de connexion a été soumis
if ( $request->has( 'Login' ) && $request->has( 'Password' ) ) {
$login = $request->get( 'Login' );
$password = $request->get( 'Password' );
if ( trim( $login ) != '' && trim( $password ) != '' ) {
$userService = $this->getRepository()->getUserService();
try {
$user = $userService->loadUserByCredentials( $login, $password );
return $this->connectUser( $user, $redirectURI );
} catch (NotFoundException $e) {
$failLogin = true;
}
} else {
$failLogin = true;
}
}
return $this->render(
'AcmeMyBundle:user\connection:login.html.twig',
array(
'noLayout' => false,
'fail_login' => $failLogin,
'redirect_uri' => $redirectURI
)
);
}
/**
* Déconnecte l'utilisateur courant.
*
* @param string URI vers laquelle rediriger après la déconnexion
*
* @return \Symfony\Component\HttpFoundation\Response
*
* @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException Si l'utilisateur anonyme n'existe pas
*/
public function logoutAction( $redirectURI ) {
// Suppression d'un éventuel doublon dans l'URI
if ( ( $offset = strpos( $redirectURI, '/user/logout' ) ) === 0 ) {
$redirectURI = substr( $redirectURI, $offset );
}
// Récupération de l'utilisateur anonyme
$userService = $this->getRepository()->getUserService();
$anonymousUser = $userService->loadAnonymousUser();
return $this->connectUser( $anonymousUser, $redirectURI );
}
/**
* Connecte l'utilisateur en argument et retourne une redirection 302.
* Si l'utilisateur en argument est anonyme, alors c'est une déconnexion.
*
* @param \eZ\Publish\API\Repository\Values\User\User $user L'utilisateur à connecter
* @param string $redirectURI URI vers laquelle rediriger l'utilisateur après connexion
*
* @return RedirectResponse
*/
protected function connectUser( User $user, $redirectURI = '/' ) {
$repository = $this->getRepository();
$repository->setCurrentUser( $user );
$session = $this->getRequest()->getSession();
try {
$response = new RedirectResponse( $redirectURI );
} catch (NotFoundException $e) {
$response = new RedirectResponse( '/' );
}
$userService = $repository->getUserService();
$anonymousUser = $userService->loadAnonymousUser();
// Si l'utilisateur en argument est anonyme
if ( $user->id === $anonymousUser->id ) {
// Déconnexion de l'utilisateur courant
$response->headers->setCookie( new Cookie( 'is_logged_in', 'false' ) );
$session->remove( 'eZUserLoggedInID' );
} else {
// Connexion de l'utilisateur
$response->headers->setCookie( new Cookie( 'is_logged_in', 'true' ) );
$session->set( 'eZUserLoggedInID', $user->id );
}
return $response;
}
}
Explications :
eZ\Bundle\EzPublishCoreBundle
fourni par eZ Publish.Action
.
Elles retournent un objet Response
.loginAction()
inclue le template de login dans sa réponse, grâce aux lignes :return $this->render(
'AcmeMyBundle:modules/user:login.html.twig',
array(
'noLayout' => false,
'fail_login' => $failLogin,
'redirect_uri' => $redirectURI
)
);
connectUser()
qui connecte l'utilisateur
et on fournit une réponse de type redirection.Pour que Symfony sache quoi faire lorsqu'on appelle la page http://mon-site/user/login,
il faut modifier le fichier Resources/config/routing.yml
dans votre bundle.
Ajoutez-y les deux routes suivantes, pour se connecter et se déconnecter :
acme_my_user_login:
pattern: /user/login{redirectURI}
defaults: { _controller: AcmeMyBundle:UserConnection:login }
requirements:
redirectURI: ".*"
acme_my_user_logout:
pattern: /user/logout{redirectURI}
defaults: { _controller: AcmeMyBundle:UserConnection:logout }
requirements:
redirectURI: ".*"
Explications :
acme_my_user_login
).
C'est lui qui est appelé dans la fonction path()
côté template./user/login<quelque chose>
, la méthode loginAction()
du contrôleur UserConnectionController
du bundle AcmeMyBundle
sera exécutée.
La méthode recevra quelque chose
en argument.requirements
permettent entre autres de spécifier la forme que doit avoir le paramètre.
Ici, n'importe qu'elle chaîne est autorisée.Modifié le :
Publié le :
Dans les JSTL, la locale permet de déterminer comment formater une date ou un nombre. Par défaut, c'est la JVM qui détermine cette valeur.
Pour modifier cette valeur pour toute une session, il suffit d'utiliser :
<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt" %>
<fmt:setLocale value="fr_FR" scope="session" />
Vous pouvez aussi paramétrer cette valeur pour toute une portlet en ajoutant ces lignes au web.xml, directement sous la racine <web-app>
:
<context-param>
<param-name>javax.servlet.jsp.jstl.fmt.locale</param-name>
<param-value>fr_FR</param-value>
</context-param>
Modifié le :
Publié le :
Si vous avez l'erreur suivante en commitant sur SVN, c'est sans doute à cause d'un caractère spécial dans votre commentaire.
Command Commit
Error Commit failed (details follow):
Error At least one property change failed; repository is unchanged
Error Server sent unexpected return value (409 Conflict) in response to PROPPATCH
Error request for
Error '/subversion/folder/!svn/ayt/r45g987g-h111-ty78-96x3-55gt9pljmo4r/0123'
Completed!
Modifié le :
Publié le :
Par défaut, les boîtes de dialogue de jQuery UI sont centrées verticalement et horizontalement. Cependant, le centrage vertical n'est pas très intelligent et agit par rapport à la hauteur de l'écran, peu importe où on est dans la page.
Imaginons par exemple que votre écran fasse 1200px
de hauteur et votre page 2000px
.
Placez un bouton qui ouvre la pop-up tout en bas de la page, scrollez jusqu'à lui et cliquez dessus.
La pop-in s'ouvrira à 600px
(environ) du haut de la page, alors que vous serez tout en bas.
Voici une astuce pour la placer à hauteur du bouton :
jQuery('#my-button').on('click', function(event) {
event.preventDefault();
jQuery('#my-dialog-confirm').dialog({
"resizable": false,
"draggable": false,
"modal": true,
"buttons" : {
"Oui" : function() {
jQuery(this).dialog("close");
// Traitement à effectuer
// [...]
},
"Non" : function() {
jQuery(this).dialog("close");
}
}
});
// Positionnement de la pop-in
var topOffset = jQuery(this).offset().top - (jQuery('#my-dialog-confirm').height() / 2);
jQuery('#my-dialog-confirm').dialog( 'option', 'position', ['center', topOffset] );
});
Explications :
Modifié le :
Publié le :
Si vous permettez à des contributeurs de saisir un texte avec des retours à la ligne (= textarea), vous aurez sans doute besoin de les afficher ensuite.
Il suffit de remplacer les retours charriots Java (\n
) en retour à la ligne HTML (<br />
) :
<c:set var="newline" value="<%= \"\n\" %>" />
${fn:replace(myAddress, newline, "<br />")}
Remarque :
Pour pouvoir utiliser la fonction de remplacement de JSTL, vous devez ajouter cet import :
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
Modifié le :
Publié le :
Pour mettre à jour les dépendances d'un projet Symfony (ou pas), vous pouvez utiliser Composer.
À l'instar d'apt
ou yum
pour des distributions Linux,
cet outil va mettre à jour votre application par une simple ligne de commande.
Pour mettre à jour Composer, utilisez :
composer self-update
Pour mettre à jour les dépendances du projet, utilisez :
composer update
Pour ne mettre à jour qu'une seule dépendance, ajoutez-la à la commande :
composer update ma/dépendance
Remarques :
composer.json
.composer update
: va chercher la dernière version disponible sur le dépôt et l'installer, composer install
: va chercher la version installée lors du dernier composer update
sur le dépôt
et l'installe. Ce numéro de version est stocké dans le fichier composer.lock
.
Cela permet d'avoir une version identique entre chaque environnement. Modifié le :
Publié le :
Si vous êtes dans une iframe et que vous souhaitez rediriger la page qui la contient, vous pouvez utiliser ces quelques lignes :
var url = "https://targeturl.com"
// Si on est dans une iframe
if (frameElement) {
window.top.location.href = url;
}
Remarque : Ce code javascript doit se trouver dans l'iframe.
Modifié le :
Publié le :
Si vous avez besoin de chiffrer une chaîne de caractères en Java, vous pouvez utiliser ces deux méthodes :
/**
* Chiffre la chaîne en argument en utilisant la clé fournie.
* Ajoute le suffixe à la chaîne chiffrée puis convertit le tout en base64.
*
* @param string Chaîne à chiffrer
* @param keyString Clé de chiffrement
* @param suffix Suffixe à ajouter à la chaîne (ex: la date au format yyyyMMdd)
* @return la chaîne chiffrée + le suffixe, en base64
* @throws RESTTechnicalException
*/
public static String encrypt(String string, String keyString, String suffix) throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException, UnsupportedEncodingException {
String encryptedB64String = null;
// Chiffrement de la chaîne
Key key = new SecretKeySpec(keyString.getBytes("UTF-8"), "Blowfish");
Cipher cipher = Cipher.getInstance("Blowfish");
cipher.init(Cipher.ENCRYPT_MODE, key);
byte[] encryptedString = cipher.doFinal(string.getBytes("UTF-8"));
// Ajout du suffixe
encryptedString = JavaUtil.mergeArray(encryptedString, suffix.getBytes());
// Encodage de la chaîne en base 64
encryptedB64String = Base64.encodeBase64String(encryptedString);
return encryptedB64String;
}
/**
* Déchiffre la chaîne en argument en utilisant la clé fournie.
*
* @param b64String Chaîne chiffrée + le suffixe, en base 64
* @param keyString Clé de déchiffrement
* @param suffix Suffixe à ajouter à la chaîne (ex: la date au format yyyyMMdd)
* @return la chaîne déchiffrée
* @throws RESTTechnicalException
*/
public static String decrypt(String b64String, String keyString, String suffix) throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException, UnsupportedEncodingException {
String decryptedString = null;
// Décodage de la chaîne en base 64
byte[] string = Base64.decodeBase64(b64String);
// Suppression du suffixe
string = JavaUtil.truncateArray(string, suffix.length());
// Déchiffrement de la chaîne
Key key = new SecretKeySpec(keyString.getBytes("UTF-8"), "Blowfish");
Cipher cipher = Cipher.getInstance("Blowfish");
cipher.init(Cipher.DECRYPT_MODE, key);
decryptedString = new String(cipher.doFinal(string));
return decryptedString;
}
Explications :
Remarques :
Le code des méthodes JavaUtil.truncateArray()
et JavaUtil.mergeArray()
est disponible ci-dessous :
/**
* Tronque le tableau de byte en arguments.
*
* @param array Tableau à tronquer
* @param maxSize Taille maximale du tableau à conserver
* @return le tableau tronqué
*/
public static byte[] truncateArray(byte[] array, int maxSize) {
int newArraySize = Math.min(array.length, maxSize);
byte[] newArray = new byte[newArraySize];
System.arraycopy(array, 0, newArray, 0, maxSize);
return newArray;
}
/**
* Merge les deux tableaux en arguments.
*
* @param first Premier tableau
* @param second Second tableau
* @return
*/
public static byte[] mergeArray(byte[] first, byte[] second) {
byte[] result = Arrays.copyOf(first, first.length + second.length);
System.arraycopy(second, 0, result, first.length, second.length);
return result;
}
Modifié le :
Publié le :
Il arrive qu'on récupère un tableau d'objets tous liés entre eux, alors qu'on voudrait un arbre.
Imaginons par exemple que le tableau contienne des objets Node
,
ayant entre autres les attributs id
et parentNodeId
.
La structure qu'on souhaite ressemblerait à :
Array (
[0] => stdClass Object (
[node] => Node object (
[...]
)
[children] => Array (
[0] => stdClass Object (
[node] => Node object (
[...]
)
[children] => Array (
[0] => stdClass Object (
[node] => Node object (
[...]
)
[children] => Array()
)
)
)
)
)
[1] => stdClass Object (
[node] => Node object (
[...]
)
[children] => Array()
)
)
Voici la fonction qui permet de passer du tableau à l'arbre :
/**
* Construit un arbre à partir du tableau de nœuds en argument.
*
* @param array $node_list Tableau de nœuds
* @return array Tableau d'objets avec deux attributs :
* - node : Nœud
* - children : Tableau d'objets de ce type
*/
public static function buildTreeNode(array $node_list) {
$tree = null;
$children = [];
// Création d'une structure exploitable
$node_std_object_list = [];
foreach ($node_list as $node) {
$node_std_object_list[] = (object) [
'id' => $node->id,
'parentNodeId' => $node->parentNodeId,
'node' => $node,
'children' => []
];
}
// Mémorisation des liens entre les nœuds
foreach ($node_std_object_list as $node_std) {
$children[$node_std->parentNodeId][] = $node_std;
}
// Création de l'arbre
foreach ($node_std_object_list as $node_std) {
if (isset($children[$node_std->id])) {
$node_std->children = $children[$node_std->id];
}
unset($node_std->id);
unset($node_std->parentNodeId);
}
if (!empty($children)) {
$tree = array_shift($children);
}
return $tree;
}
Explications :
children
de tous ces objets standards.
On a alors la liste de tous les nœud avec pour chacun d'eux ses enfants.Remarques :
Object
en java, il existe la classe stdClass
(Standard Class) pour
représenter des objets.stdClass
et vice-versa.Modifié le :
Publié le :
Si vous avez perdu le mot passe administrateur, ou si vous souhaitez le modifier directement dans le store, ajoutez une ligne de la forme suivante à la fin :
<member stamp="j_99999" id="j_2" op="update" author="" mdate="1091526019085" mobile="" name="Admin" password="ISMvKXpXpadDiUoOSoAfww==" />
Cette ligne remplace le mot de passe actuel par admin
.
Remarques :
stamp
par la même valeur que votre dernière ligne de store, en l'incrémentant.mdate
).Sur JPlatform 10, le chiffrement des mots de passe a changé. La ligne à ajouter pour le réinitialiser devient :
<member stamp="agi_7217" id="j_2" op="update" mdate="1528122795429" login="admin" name="Admin" password="$2a$10$gbCw7UaAI1kL7PXqodTY9OmzqsdFBufD063oG6ebQT/Zi2PBELX56" />
Modifié le :
Publié le :
Lorsque vous faites flotter un bloc en CSS, le suivant essaiera de s'afficher à droite du premier (avec float: left;
),
ou à sa gauche (avec float: right;
).
Si vous voulez qu'il s'affiche à la ligne, vous avez plusieurs solutions :
clear: left
.La propriété clear
peut prendre principalement 4 valeurs : none
(par défaut), left
, right
et both
.
left
, et qu'un flottant se trouve à sa gauche, le bloc ira à la ligne.right
, et qu'un flottant se trouve à sa droite, le bloc ira à la ligne.both
, et qu'un flottant se trouve à sa droite ou à sa gauche, le bloc ira à la ligne.none
, qu'un flottant se trouve à côté et qu'il a la place il se positionnera à côté.Exemple d'utilisation :
<div style="width: 90px;">Mon bloc conteneur avec une largeur de 90px</div>
<div style="float: left; width: 30px;">Mon premier flottant</div>
<div style="float: left; width: 30px;">Mon deuxième flottant</div>
<div style="clear: left; width: 30px;">Mon bloc à afficher en dessous</div>
</div>
Explication :
Le troisième bloc a la place de s'afficher à droite des deux premiers.
clear: left;
, il s'affiche à côté d'eux.clear: left;
, il s'affiche en dessous.Modifié le :
Publié le :
Bundle est le nom Symfony pour un module, une brique, une extension (terme eZ Publish 4.X). Il peut contenir tout ou une partie du design, une API technique, une fonctionnalité particulière, ...
Il peut être dépendant d’autres Bundles, mais est réutilisable. Il est identifié par un nom de domaine (namespace)
et un nom de Bundle (finissant par Bundle
), le tout concaténé.
La création du Bundle peut se faire via ligne de commande, à partir du répertoire racine de l’application eZ Publish :
php ezpublish/console generate:bundle --namespace="MonNamespace/MonBundle"
Explications :
namespace
est le nom du namespace et le nom du Bundle concaténés./
.Bundle
.L’assistant de création du Bundle propose alors le nom final du Bundle : MonNamespaceMonBundle
.
Appuyez sur Entrée
pour conserver ce nom standard.
Remarque :
Si vous créez le Bundle principal de votre application, et que vous souhaitez avoir un namespace et
un nom de Bundle identique, c'est à cette étape que vous pouvez simplifier le nom final pour
éviter d'avoir MonNamespaceMonNamespaceBundle
.
Vous pouvez ensuite choisir le chemin où se trouvera le Bundle.
Par défaut il sera créé dans le répertoire src/
à la racine de l’application eZ Publish,
ce qu'il est préférable de conserver : appuyez sur Entrée
.
Choisissez yml
comme format de configuration, et validez avec Entrée
.
Pour une première fois, choisissez de générer la structure complète du Bundle.
Appuyez sur Entrée
pour confirmer toute la génération.
Même chose pour les questions suivantes.
L’assistant de création de Bundle a généré l’arborescence suivante :
Comme tout bundle Symfony, il se compose de 3 répertoires principaux :
Controller/
: vous y créerez vos contrôleurs (équivalents de vos modules dans eZ4).Resources/
: s'y trouvent tous les fichiers non PHP (templates Twig, fichiers de config, js, CSS, ...).Tests/
: répertoire contenant vos tests unitaires.Modifié le :
Publié le :
Modifié le :
Publié le :
Si vous utilisez la méthode parseInt()
pour parser des chaînes de caractères représentant des nombres commençant par 0,
vous pouvez rencontrer ce problème :
var test;
test = parseInt("01"); // test = 1
test = parseInt("02"); // test = 2
test = parseInt("03"); // test = 3
test = parseInt("04"); // test = 4
test = parseInt("05"); // test = 5
test = parseInt("06"); // test = 6
test = parseInt("07"); // test = 7
test = parseInt("08"); // test = 0
test = parseInt("09"); // test = 0
Sous Internet Explorer 8 et inférieur, la méthode parseInt()
considère les nombres commençant par 0 comme étant en base 8.
Il faut donc utiliser le second paramètre de la méthode, pour préciser qu'on veut travailler en base 10 :
var test;
test = parseInt("01", 10); // test = 1
test = parseInt("02", 10); // test = 2
test = parseInt("03", 10); // test = 3
test = parseInt("04", 10); // test = 4
test = parseInt("05", 10); // test = 5
test = parseInt("06", 10); // test = 6
test = parseInt("07", 10); // test = 7
test = parseInt("08", 10); // test = 8
test = parseInt("09", 10); // test = 9
Modifié le :
Publié le :
Pour savoir quelle option de votre liste déroulante est sélectionnée, vous pouvez utilisez quelque chose comme ça :
jQuery('select[name="mySelect"] option:selected').val();
ou
jQuery('#mySelectID').find('option:selected').val();
Modifié le :
Publié le :
Lorsque vous affichez un document JSON dans Firefox, vous obtenez quelque chose comme :
{"errorMessageCustom":null,"errorCodeCustom":0,"errorMessage":null,"errorCode":0,"results":[{"message":"Le cache des requêtes sql a bien été rechargé."}],"nbResults":1,"ok":true}
Si vous êtes en train de tester des services web qui retournent du JSON et que vous voulez afficher les résultats, cette extension Firefox vous sera très utile : JSON Lite.
Pour le même JSON, vous obtiendrez un affichage bien plus agréable :
Modifié le :
Publié le :
Voici une petite fonction pour scroller automatiquement vers le haut jusqu'à un élément :
/**
* Scrolle automatiquement vers le haut jusqu'à l'élément dont le sélecteur est en argument.
*
* @param selector string Sélecteur de l'élément vers lequel scroller
* @param nbMs Nombre de millisecondes pour effectuer l'animation de scroll (facultatif, 10 par défaut)
*/
function scrollToElement(selector, nbMs) {
nbMs = nbMs || 10;
var documentBody = (($.browser.chrome)||($.browser.safari)) ? document.body : document.documentElement;
jQuery(documentBody).animate({scrollTop: jQuery(selector).offset().top}, nbMs, 'linear');
}
Remarques :
browser
a été retirée et transférée dans le plugin
jQuery Migrate. Il faut donc ajouter ce plugin pour pouvoir l'utiliser.Modifié le :
Publié le :
JQuery UI fournit un datepicker pour agrémenter les champs date d'un petit calendrier. Malheureusement il ne propose pas l'équivalent pour un champ heure.
C'est ce que propose ce site : http://trentrichardson.com/examples/timepicker/.
Ce timepicker étends jQuery UI et permet de le combiner ou non avec le datepicker déjà existant.
Remarque :
Le timepicker a été traduit dans de nombreuses langues. Les traductions sont disponibles sur ce repo git.
Si vous utilisez le framework Bootstrap, ce site propose un autre timepicker un peu plus agréable à utiliser : http://jdewit.github.io/bootstrap-timepicker/.
Modifié le :
Publié le :
Si vous rencontrez l'erreur suivante après l'installation d'eZ Publish 5 :
FatalErrorException: Error: Class 'XSLTProcessor' not found in
[...]\vendor\ezsystems\ezpublish-kernel\eZ\Publish\Core\FieldType\XmlText\Converter\Html5.php line 77
C'est que l'extension xsl
n'est pas activée pour PHP.
Modifié le :
Publié le :
Lorsque vous migrez vers eZ Publish Community Project 2013.06, vous pouvez rencontrer cette erreur :
The extension "ext/fileinfo" must be loaded in order for this class to work.
Fileinfo est une extension pour PHP. Elle est généralement déjà packagée sous Linux, mais pas sous Windows avec WampServer.
Activez l'extension php_fileinfo
et redémarrez apache.
Modifié le :
Publié le :
Dans eZ Publish 4, un datatype est un type de champ pour une classe de contenu.
EZ Publish en fournit un certain nombre : image, ligne de texte, texte riche, case à cocher, nombre, ... Vous pouvez également créer votre propre datatype, pour facilité la contribution et l'affichage d'un champ.
On peut par exemple imaginer un champ couleur, avec côté back-office une pipette ou une palette de couleur pour choisir facilement sa couleur.
Voici un tutoriel complet pour créer son propre datatype, rédigé par Jérôme Vieilledent et Nicolas Pastorino pour PHP Solutions.
Modifié le :
Publié le :
Si vous créez des webservices et les distribuez, il est utile de fournir également la documentation expliquant comment les utiliser.
Il existe un outil proposant une interface web pour tester des webservices, qui détaille également leur utilisation : Swagger.
Cet outil gère tous les types de requête HTTP (GET
, POST
, PUT
, DELETE
, ...).
Il permet d'envoyer des données au format JSON ou autre, d'afficher les réponses avec coloration syntaxique, les codes d'erreur, ...
Chaque webservice est détaillé dans une structure de données au format JSON. Cette structure peut être générée automatiquement pour de nombreux langages et Frameworks, notamment en utilisant des annotations particulières dans vos méthodes.
Modifié le :
Publié le :
Si vous utilisez un mapping JSON en Java, vous pouvez rencontrer l'erreur suivante :
org.codehaus.jackson.map.JsonMappingException: No suitable constructor found for type
Cette erreur se produit si vous utilisez un Bean global contenant une ou plusieurs classes internes. Exemple :
public class MyGlobalClass {
private List<SubClass> subClassList;
public class SubClass {
private String id;
}
}
Pour ne pas avoir cette erreur, déclarez vos classes internes statiques dans la classe globale :
public static class SubClass {
private String id;
}
Modifié le :
Publié le :
Si dans un formulaire vous avez deux boutons radio "Oui"/"Non et que vous avez besoin de savoir lequel est sélectionné, vous pouvez utiliser la fonction suivante :
/**
* Retourne si l'élément radio est à true.
*
* @param elementName Nom de l'élément (attribut "name")
*/
function isChecked(elementName) {
var element = jQuery('input[name=' + elementName + ']:checked');
return element != null && element.val() == 1;
}
Remarques :
1
et l'autre 0
.element.val() == 1
par une autre valeur que 1
.Modifié le :
Publié le :
Si vous utilisez une iframe dans une page, vous pouvez fixer sa largeur et sa hauteur. Si vous ne le faite pas, ses dimensions seront limitées par le navigateur et tout le contenu de l'iframe ne sera pas forcément visible sans scroller.
Pour agrandir l'iframe en fonction de son contenu, vous pouvez utiliser cette fonction :
/**
* Redimensionne automatiquement la hauteur de l'iframe contenant la page courante.
* (La page courante est celle dans laquelle est appelée cette fonction.)
*
* @param marginHeight Marge verticale en pixel à appliquer (facultatif, 25 par défaut)
*/
function autoResizeIFrame(marginHeight) {
marginHeight = marginHeight || 25;
var id;
// Si la page courante est dans une iframe
if (frameElement) {
try {
id = frameElement.attributes['id'].value;
} catch (e) {
if (window.console && console.log) {
console.log(e.message);
}
}
}
// Si la page contenant l'iframe a un ID
if (id) {
if (document.getElementById) {
// Firefox
if (navigator.appName == 'Netscape') {
parent.document.getElementById(id).style.height = this.document.body.offsetHeight + marginHeight + 'px';
// IE
} else {
marginHeight += 5;
parent.document.getElementById(id).style.height = this.document.body.scrollHeight + marginHeight + 'px';
}
// Opera
} else if (!window.opera && !document.mimeType && document.all && document.getElementById) {
marginHeight -= 7;
parent.document.getElementById(id).style.height = this.document.body.offsetHeight + marginHeight + 'px';
}
// Retour au haut de la page
window.parent.scrollTo(0, 0);
}
}
Pour l'utiliser, il suffit d'appeler la fonction depuis l'iframe, à la fin du chargement de la page (ou après avoir modifier le contenu de l'iframe via Javascript).
Remarque :
Cette fonction n'est utilisable que si la page qui inclue l'iframe et la page à l'intérieur ont le même domaine. Cette restriction est une sécurité propre à Javascript pour éviter à une page de modifier le contenu d'une autre.
Modifié le :
Publié le :
Il est simple de tester un webservice via un navigateur web lorsque l'appel est une requête de type GET
,
mais c'est beaucoup moins évident pour les requêtes POST
, PUT
et autres.
L'extension RESTClient de Firefox permet de faire tout cela.
Grâce à elle, vous pouvez envoyer des données au format texte, JSON ou autre, et afficher la réponse de manière lisible. Il est possible d'ajouter des header personnalisés, ou de s'authentifier auprès du webservice.
Si le webservice retourne du JSON, la réponse sera présentée avec indentation et coloration syntaxique.
De plus, vous pouvez sauvegarder vos requêtes et les réutiliser par la suite.
Modifié le :
Publié le :
Dans un formulaire, vous pouvez brider les éléments input, pour ne pas qu'ils soient modifiables par les utilisateurs.
Cela revient au même qu'un champ de type hidden, sauf qu'il sera visible par l'utilisateur.
Malheureusement cet attribut ne fonctionne que sur les champs input de type text
.
<!-- Champ texte : le readonly fonctionne -->
<input type="text" name="champ1" id="champ1" value="valeur1" readonly="readonly" />
<!-- Champ checkbox : le readonly ne fonctionne pas -->
<input type="checkbox" name="champ2" id="champ2" value="valeur2" readonly="readonly" />
Pour simuler le même fonctionnement avec un champ input de type checkbox
, vous pouvez utiliser l'attribut disabled
:
<!-- Champ checkbox avec disabled -->
<input type="checkbox" name="champ2" id="champ2" value="valeur2" disabled="disabled" />
<input type="hidden" name="champ2" id="champ2Hidden" value="valeur2" />
Explications :
disabled
, sa valeur ne sera pas envoyée par le navigateur à la page cible du formulaire. hidden
, avec le même attribut name
et la même valeur.
C'est lui qui enverra la donnée.Modifié le :
Publié le :
Lorsque vous créez une nouvelle classe en Java, elle hérite de la méthode toString() de la classe Object. Par défaut, cette méthode n'affiche rien de bien compréhensible.
Vous pouvez la surcharger pour afficher lisiblement les valeurs de tous les champs de votre objet de cette manière :
@Override
public String toString() {
return ToStringBuilder.reflectionToString(this, ToStringStyle.MULTI_LINE_STYLE);
}
Grâce à l'introspection et à la librairie org.apache.commons, ces quelques lignes de code fonctionneront pour toutes vos classes.
Modifié le :
Publié le :
PEAR est un gestionnaire de librairies pour PHP, permettant d'étendre les fonctionnalités de PHP par simple ligne de commande. Il tend toutefois à disparaître au profit de Composer.
Si le fichier go-pear.bat
n'est pas présent dans le répertoire de PHP de WampServer :
go-pear.phar
(http://pear.php.net/go-pear.phar).D:\Dev\wamp\bin\php\php5.3.13\
).Une fois le fichier batch en place, lancez l'invite de commande et exécutez-le :
cd D:\Dev\wamp\bin\php\php5.1.13\
go-pear
Durant l'installation, appuyez toujours sur Entrée
ou Y
, pour utiliser les paramètres par défaut.
Lancez le fichier PEAR_ENV.reg
présent dans le répertoire PHP de WampServer,
pour mettre à jour les clés de registre nécessaires. (Double-cliquez sur le fichier pour le lancer.)
Pour mettre à jour votre installation, et vérifier que PEAR est bien installé, exécutez la commande suivante :
pear channel-update pear.php.net
Modifié le :
Publié le :
Pour pouvoir utiliser simplement subversion en PHP, vous pouvez utiliser la librairie VersionControl_SVN.
Remarque :
Pour git, la librairie VersionControl_Git existe également et s'installe de la même façon.
Exécutez juste la commande suivante :
pear install VersionControl_SVN
En cas d'erreur :
Failed to download pear/VersionControl_SVN within preferred state "stable", latest release is version 0.5.0,
stability "alpha", use "channel://pear.php.net/VersionControl_SVN-0.5.0" to install
install failed
exécutez plutôt la commande suivante pour indiquer le canal d'installation à utiliser :
pear install channel://pear.php.net/VersionControl_SVN-0.5.0
Le fichier Diff.php
de la librairie comporte une erreur.
Il se trouve dans le répertoire VersionControl\SVN\Command\
du répertoire PEAR
(par exemple : D:\Dev\wamp\bin\php\php5.3.13\pear\VersionControl\SVN\Command\
).
Tout à la fin du fichier, remplacez summerize
par summarize
(il était temps que l'été arrive apparemment :).
Modifié le :
Publié le :
Google Analytics fonctionne très bien même en intranet, tant que les utilisateurs du site ont accès à internet. Cela n'est vrai qu'à une condition : le site intranet doit avoir un nom de domaine finissant par ".qqchose" (.fr, .com, ...).
Par exemple, http://mon-intranet.fr
, http://mon-intranet.pwet
fonctionneront, mais pas http://mon-intranet
.
Dans ce cas particulier, vous pouvez quand même utiliser Google Analytics en ajoutant cette ligne au code javascript fourni par Google :
_gaq.push(['_setDomainName', 'none']);
Modifié le :
Publié le :
Par défaut, JCMS utilise log4j pour gestionnaire de logs.
Voici comment l'utiliser dans une classe :
public class MyClass {
/** Logger log4j */
private static final Logger logger = Logger.getLogger(MyClass.class);
public MyClass() {
logger.info("Je suis dans le constructeur par défaut de ma classe.");
}
Explications :
getLogger()
de log4j.debug()
, info()
, warn()
, error()
ou fatal()
, avec le message en argument, plus éventuellement une exception.Log4j est configuré via le fichier log4j.xml
. Dans JCMS, ce fichier se trouve dans WEB-INF/data/
.
Si dans un nouveau projet JCMS vous créez un package pour y mettre vos classes Java, il est probable que les logs ne fonctionnent pas. En effet, log4j n'a pas connaissance de ce nouveau package.
Pour l'en informer, éditez le fichier log4j.xml
et ajoutez :
<!-- Logger de classes personnalisées -->
<logger name="mon.package" additivity="false">
<level value="DEBUG" />
<appender-ref ref="JCMS" />
<appender-ref ref="CONSOLE" />
<appender-ref ref="LOGFILE" />
<appender-ref ref="PLUGIN" />
</logger>
Explications :
name
définit pour quels packages utiliser log4j.
(Les sous-packages seront automatiquement logués.)value
de l'élément <level>
permet de déterminer quel niveau de log appliquer.
En développement on utilise souvent DEBUG
ou INFO
alors qu'en production on se contentera
de INFO
ou WARN
. Concrètement, avec le niveau WARN
, les méthodes logger.debug()
et logger.info()
n'auront aucun effet (suivant cet ordre : debug
< info
< warn
< error
< fatal
).<appender>
sont les sorties à utiliser : fichier de log, console, ...log4j.xml
.Modifié le :
Publié le :
La taille des fichiers uploadés via PHP est limitée.
Par défaut PHP fixe cette limite à 2Mo
, mais cela est bien sûr configurable.
Pour cela, éditez le fichier php.ini
et modifiez la propriété upload_max_filesize
avec la
taille que vous souhaitez (ex: 16M).
Deux autres propriétés peuvent brider la taille maximale :
post_max_size
: Nombre d'octets transmissibles via une requête POST
(si vous voulez uploader un fichier via un formulaire).memory_limit
: Nombre d'octets que PHP peut stocker en mémoire.Ces deux propriétés doivent être supérieures ou égales à upload_max_filesize
.
On est souvent confronté à cette limite lorsqu'on utilise PHPMyAdmin pour uploader un dump SQL.
Par exemple, si vous avez post_max_size: 3M
et upload_max_filesize: 16M
, vous ne pourrez pas
uploadé un fichier de plus de 3Mo.
Modifié le :
Publié le :
Lorsque vous créez un DataController et que vous surchargez les méthodes beforeWrite()
,
checkWrite()
et afterWrite()
, le dernier paramètre de la méthode (la Map
),
contient tout le contexte disponible.
Dans cette variable il y a notamment :
request
: L'objet HttpServletRequest
de tomcat.response
: L'objet HttpServletResponse
de tomcat.formHandler
: Le FormHandler
utilisé lors de l'édition du contenu.Et dans le cas d'une modification de donnée, il y a également :
Data.previous
: La dernière version de la Data
avant modification.Modifié le :
Publié le :
Si vous avez besoin de comparer l'ancienne et la nouvelle version d'une Data
,
dans un DataController (par exemple dans afterWrite()
),
vous aurez besoin d'utiliser le contexte en paramètre de la méthode.
Pour obtenir cette version, utilisez quelque chose comme :
final Article oldArticle = (Article) context.get("Data.previous");
Modifié le :
Publié le :
Dans vos classes de test, vous pouvez utiliser le gestionnaire de log slf4j Il s'agit d'une façade qui peut utiliser plusieurs loggers au choix, comme log4j par exemple.
Voici comment utiliser ce gestionnaire :
public class MyClassTest extends JcmsTestCase {
/** Logger **/
private static Logger logger;
/**
* Initialise un contexte pour une suite de tests, avec des méthodes d'initialisation et de finalisation.
*
* @return une configuration de suite de tests
*/
public static TestSetup suite() {
return new TestSetup(new TestSuite(MyClassTest.class)) {
@BeforeClass
@Override
protected void setUp() throws Exception {
logger = LoggerFactory.getLogger(MyClassTest.class);
logger.debug("Début des tests unitaires pour la classe " + MyClass.class.getSimpleName() + ".");
}
@AfterClass
@Override
protected void tearDown() throws Exception {
logger.debug("Fin des tests unitaires.");
}
};
}
Explications :
suite()
, pour pouvoir initialiser des éléments au début des tests.LoggerFactory.getLogger()
, pour initialiser le logger.logger.info()
, logger.debug()
, ...).Remarque :
Pour pouvoir utiliser slf4j avec log4j, vous devez inclure ces deux jars dans votre projet : slf4j-api-1.5.8.jar et slf4j-log4j12-1.5.8.jar.
Modifié le :
Publié le :
Si vous utilisez Derby dans vos tests unitaires, vous aurez sans doute besoin de réinitialiser votre base entre chaque test.
Pour supprimer le contenu de toutes les tables d'un coup, vous pouvez utiliser cette fonction :
/**
* Vide toutes les tables de la base de données, dont le nom commence par "G_" ou "J_".
*
* @throws SQLException
*/
public static void derbyClearAllTables() throws SQLException {
ResultSet tables;
Statement stat;
final String schema = "jdbc:derby:jcmsdbunit";
// Récupération de la liste des tables de la base de données
final Connection connection = DriverManager.getConnection(schema);
tables = connection.getMetaData().getTables(null, null, null, null);
final Set<String> tableNameSet = new HashSet<String>();
// Parcours des tables de la base de données
while (tables.next()) {
final String tableName = tables.getString("TABLE_NAME");
if (tableName.startsWith("G_") || tableName.startsWith("J_")) {
tableNameSet.add(tableName);
}
}
// Parcours des tables à vider
for (final String tableName : tableNameSet) {
// Nettoyage de la table
stat = connection.createStatement();
stat.executeUpdate("DELETE FROM " + tableName);
connection.commit();
}
if (!tables.isClosed()) {
tables.close();
}
}
Remarque :
Les tables ne commençant ni par G_
ni par J_
sont probablement des tables utiles au fonctionnement
interne de Derby. Elles ne sont pas à vider.
Modifié le :
Publié le :
Pour utiliser les tests unitaires et JUnit dans vos projets JCMS, Jalios fournit quelques classes utilitaires,
dont JcmsTestCase
.
Plus de détails dans les articles suivants :
Modifié le :
Publié le :
Pour effectuer un traitement une fois au début des tests, on utilise généralement la méthode suivante dans la classe de test :
@BeforeClass
public static void oneTimeSetUp() {
logger = LoggerFactory.getLogger(MyClassTest.class);
logger.info("Début des tests unitaires.");
}
Cela permet d'initialiser un logger, configurer l'environnement à utiliser pour tous les tests, ...
Cette méthode ne semble pas fonctionner avec JCMS (dans eclipse en tous cas).
Vous pouvez faire l'équivalent grâce à la méthode suite()
à ajouter dans votre classe de test :
/**
* Initialise un contexte pour une suite de tests, avec des méthodes d'initialisation et de finalisation.
*
* @return une configuration de suite de tests
*/
public static TestSetup suite() {
return new TestSetup(new TestSuite(MyClassTest.class)) {
@Before
@Override
protected void setUp() throws Exception {
logger = LoggerFactory.getLogger(MyClassTest.class);
logger.info("Début des tests unitaires.");
}
@After
@Override
protected void tearDown() throws Exception {
logger.info("Fin des tests unitaires.");
}
};
}
Remarque :
La méthode tearDown()
permet d'effectuer un traitement une fois après tous les tests.
Modifié le :
Publié le :
Dans un environnement multithread, le comportement d'un singleton peut être faussé.
Pour éviter ce problème, il suffit d'ajouter le qualificatif synchronized à la méthode du singleton :
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
Modifié le :
Publié le :
Avec eZ Publish 5, pour activer la console de développement de Symfony vous devez modifier la configuration Apache, à priori dans votre virtual host.
Remplacez index.php
par index_dev.php
:
DirectoryIndex index.php
...
RewriteRule .* /index.php
Remarque :
EZ Publish et Symfony doivent également être configurés en mode développement.
Modifié le :
Publié le :
Avec eZ Publish 5, le mot de passe de la base de données est stocké à deux endroits : dans eZ et dans Symfony.
Si tout fonctionne correctement dans le back-office, excepté l'arbre des contenus qui n'apparait pas, c'est probablement que les deux mots de passes sont différents.
Pour vérifier qu'ils sont bien configurés, vérifier dans ces deux fichiers :
ezpublish_legacy/override/site.ini.append.php
ezpublish/config/ezpublish.yml
S'il y a d'autres problèmes dans le back-office, comme l'impossibilité de naviguer dans les contenus via les éléments enfants, c'est sans doute un problème de cache.
Modifié le :
Publié le :
Un fichier encodé en UTF-8 peut avoir un caractère spécial tout au début, qu'on appelle BOM.
Certains éditeurs ont besoin du BOM pour afficher correctement du texte en UTF-8 (accents, caractères spéciaux, ...). C'est le cas de Wordpad sous Windows. D'autres plus "intelligents", comme Notepad++ et la plupart des éditeurs sous Linux, détectent l'encodage du document et ajoute le BOM si besoin.
Pour ajouter manuellement (ou programmatiquement) ce caractère, il suffit d'utiliser son code ASCII : \uFEFF
.
Sous Windows pour savoir si un fichier UTF-8 est encodé avec ou sans BOM, vous pouvez l'ouvrir avec Notepad++ et cliquer sur l'onglet Encodage.
Modifié le :
Publié le :
Le module user
d'eZ Publish fournit une vue pour stocker simplement des préférences utilisateurs :
user/preferences
.
Concrètement, ces préférences sont stockées en base de données,
avec pour clé le couple (user_id, preference_name)
et pour valeur celle de notre choix.
Ce fonctionnement est souvent utilisé en back-office, pour afficher tel ou tel bloc de la page,
comme la barre de droite par exemple.
Pour la partie publique côté front-office il est à éviter, car tous les utilisateurs anonymes auront la même
préférence. (Ou alors il ne faut pas leur permettre de modifier la valeur.)
Il suffit d'appeler l'URL user/preferences
, en proposant par exemple un lien à l'utilisateur.
Pour afficher/masquer la barre de droite du back-office, par exemple on a juste ce genre de liens :
{* Afficher la barre *}
<a href="{'user/preferences/set/admin_right_menu_show/1'|ezurl('no')}">
Afficher la barre de droite
</a>
{* Masquer la barre *}
<a href="{'user/preferences/set/admin_right_menu_show/0'|ezurl('no')}">
Masquer la barre de droite
</a>
Explication :
Pour créer/modifier une préférence, il faut appeler la vue user/preferences
, avec 3 paramètres :
set
, le nom de la préférence puis sa valeur.
Il faut utiliser la méthode setValue()
de la classe eZPreferences
:
eZPreferences::setValue( 'my_preference_name', 'my_value' );
Remarque :
Par défaut, la préférence sera associée à l'utilisateur connecté.
Un troisième argument est disponible ($user_id
), pour l'affecter à un autre utilisateur.
{ezpreference( 'my_preference_name' )}
Explication :
L'utilisateur courant est automatiquement utilisé.
Il faut utiliser la méthode value()
de la classe eZPreferences
:
eZPreferences::value( 'my_preference_name' );
Remarques :
values()
de la classe eZPreferences
permet de récupérer toutes les préférences
d'un utilisateur.Modifié le :
Publié le :
Lorsque vous écrivez la Javadoc d'une méthode, vous pouvez ajouter un lien vers une autre méthode, grâce au tag @see.
Pour cela vous devez respecter cette syntaxe :
/**
* Méthode d'exemple.
* @see {@link mypackage.MyClass#someMethod}
*/
public void myMethod() {
}
ou
/**
* Méthode d'exemple.
* @see {@link mypackage.MyClass#someMethod(Arg1Class, String)}
*/
public void myMethod() {
}
/**
* Méthode utilisée par myMethod.
*/
public void someMethod(Arg1Class arg1, String arg2) {
}
Remarque :
Pour ajouter plusieurs liens, séparez-les simplement par une virgule.
Modifié le :
Publié le :
La classe eZINI
propose deux méthodes pour récupérer des variables sous forme de tableau :
variable()
et variableArray()
.
C'est la méthode habituelle que vous utilisez pour récupérer des chaînes de caractères. Par exemple pour récupérer la valeur dans cette configuration :
[MySection]
MyProperty=value
La méthode retournera :
value1
Elle fonctionne aussi pour un tableau de valeurs :
[MySection]
MyProperty[]
MyProperty[]=value1
MyProperty[]=value2
...
La méthode retournera :
Array (
'0' => value1
'1' => value2
...
)
Cette méthode permet de récupérer un tableau de valeurs pour ce genre de configuration :
[MySection]
MyProperty=value1;value2;value3;...
La méthode retournera :
Array (
'0' => value1
'1' => value2
'2' => value3
...
)
Modifié le :
Publié le :
Pour pouvoir utiliser les données de Google Analytics directement en PHP, il existe une petite api pour communiquer simplement avec Google : GAPI.
En deux lignes de code vous pouvez par exemple récupérer le nombre de consultations par page ces 30 derniers jours :
$ga = new gapi('address@server.com', 'my_password');
// Récupération du nombre de visualisations,
// pour chaque couple (url de la page, titre de la page)
$ga->requestReportData('71538601', ['pagePath', 'pageTitle'], ['pageviews']);
Modifié le :
Publié le :
Pour pouvoir utiliser les données de Google Analytics directement en PHP, il existe une petite api pour communiquer simplement avec Google : GAPI.
En deux lignes de code, vous pouvez ainsi récupérer le nombre de visualisations par page pour les 30 derniers jours :
$ga = new gapi('address@server.com', 'my_password');
// Récupération du nombre de visualisations, pour chaque couple (url de la page, titre de la page)
$ga->requestReportData('71538601', ['pagePath', 'pageTitle'], ['pageviews']);
Modifié le :
Publié le :
Le système de grid de Bootstrap est pratique, mais parfois insuffisant. Il propose seulement 12 largeurs de colonne, ce qui limite les possibilités d'affichage.
Ce nombre de colonnes est heureusement paramétrable grâce aux fichiers .less des sources de Bootstrap. Il vous suffit de modifier et compiler ces sources pour générer une feuille Bootstrap avec le nombre de colonnes de votre choix.
Pour Bootstrap v3.0.0, il faut par exemple modifier le fichier variables.less
:
// Grid system
// --------------------------------------------------
// Number of columns in the grid system
@grid-columns: 12;
// Padding, to be divided by two and applied to the left and right of all columns
@grid-gutter-width: 30px;
// Point at which the navbar stops collapsing
@grid-float-breakpoint: @screen-tablet;
Remarque :
Si vous n'êtes pas familier avec les fichiers less, vous pouvez utiliser le formulaire de customisation de Boostrap proposé sur le site officiel. Il permet de modifier non seulement le nombre de colonnes, mais beaucoup d'autres paramètres.
Modifié le :
Publié le :
Pour qu'un site s'affiche correctement quel que soit le terminal de visualisation (mobile, tablette, ordinateur, ...), il faut adapter son code HTML et surtout son CSS. En effet, en fonction de la résolution du terminal, l'apparence d'un site peut être totalement dégradée.
Le nombre de pixels affichables par un terminal mobile ne correspond pas au nombre de pixels CSS utilisables par le navigateur. Un article complet sur le sujet est disponible sur le site alsacréations.
Concrètement, pour synchroniser ces deux "types de pixels", ajoutez la balise meta
suivante dans votre head
HTML:
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
CSS 3 introduit un nouvel élément : les media queries. Elles permettent de définir des propriétés de manière conditionnelle.
Vous pouvez par exemple définir que tout un bloc CSS ne doit être interprété que si la taille de la fenêtre est
inférieure à 400px
. Cela est très utile dans le cas du responsive, car on peut ainsi influer sur la taille
des blocs de la page, leurs marges, s'ils doivent apparaître ou non, et tout ça en fonction de la résolution du
terminal.
Voici comment utiliser les media queries directement dans vos feuilles CSS :
/* Écrans normaux - entre 860px et 1140px */
@media only screen and (min-width: 53.75em) and (max-width: 71.25em) {
/* Largeur de la page */
div#page {
width: 100%;
}
/* Menu 2 */
#menu2 {
display: none;
}
}
/* Écrans moyens - entre 592px et 859px */
@media only screen and (min-width: 37em) and (max-width: 53.6875em) {
/* Largeur de la page */
div#page {
width: 90%;
}
}
/* Petits écrans - en dessous 592px */
@media only screen and (max-width: 36.9375) {
/* Largeur de la page */
div#page {
width: 80%;
}
}
Cet exemple présente 3 fourchettes de tailles répandues, pour lesquelles l'affichage est amélioré.
Explications :
Remarques :
media
de la balise link
.Un article complet sur les media queries est présent sur le site alsacréations.
Modifié le :
Publié le :
Pour mettre des blocs côte à côte en CSS, il faut faire flotter l'un deux.
Voici un exemple simple avec une structure en 4 blocs comme on en voit souvent : un en-tête, une colonne de menus, une colonne principale et un pied de page.
Le code html est très simple :
<div id="global-container">
<div id="header">...</div>
<div id="navigation">...</div>
<div id="content">...</div>
<div id="footer">...</div>
</div>
Explication :
On a un div
pour chaque zone du site, et un autre pour le conteneur global.
Le CSS se compose également de 5 parties, une pour chaque div :
#global-container {
padding: 10%;
background-color: #CACACA;
}
#header {
height: 100px;
margin-bottom: 20px;
background-color: #868686;
}
#navigation {
height: 300px;
width: 20%;
float: left;
margin-right: 5%;
background-color: #262626;
}
#content {
height: 300px;
overflow: hidden;
background-color: #555555;
}
#footer {
height: 80px;
margin-top: 20px;
background-color: #868686;
}
Explications :
float: left
.overflow: hidden
.Modifié le :
Publié le :
Lorsque vous affichez une page sous Internet Explorer, il arrive très souvent que ce navigateur n'utilise pas le moteur de rendu le plus récent. Par exemple, vous naviguez avec IE9 et pourtant le moteur de rendu utilisé est celui d'IE7.
Cela est problématique car toutes les propriétés CSS reconnues par IE9 mais pas par IE7 ne fonctionnent donc pas.
Ce comportement peut également se produire avec Google Chrome.
Pour connaitre le moteur de rendu utilisé par IE, appuyez sur F12
(à partir de IE8).
Pour éviter ce problème, vous pouvez forcer le navigateur à utiliser son dernier moteur de rendu,
via une balise meta
dans votre head
HTML :
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
Modifié le :
Publié le :
Sous Linux, vous avez l'habitude d'utiliser la touche TAB
pour trouver automatiquement
la fin du nom d'un fichier ou d'un répertoire.
La même fonctionnalité existe pour Windows sous DOS, mais vous devez utiliser des
antislash \
au lieu des slash /
dans vos chemins.
Modifié le :
Publié le :
Sous Linux, pour savoir les tâches planifiées pour l'utilisateur courant, utilisez la commande suivante :
crontab -l
Modifié le :
Publié le :
Lorsque vous créez des mails en HTML, il est très difficile d'avoir le même rendu sur tous les logiciels de messagerie et sur tous les webmails.
Le problème vient de l’interprétation non standard du CSS, en particulier par Outlook 2007/2010/2013.
Voici la liste des propriétés reconnues par les logiciels de messagerie et webmails les plus répandues : http://www.campaignmonitor.com/css/
Modifié le :
Publié le :
Dans JCMS, la méthode getStringArrayProperty()
de la classe Channel
permet de récupérer un tableau
de valeurs définies dans une même propriété.
Par exemple :
Channel.getChannel.getStringArrayProperty("jcmsplugin.myplugin.name-list", new String[] {})
avec dans un plugin.prop
la propriété suivante :
jcmsplugin.myplugin.name-list: Jean Louis David Roger,Virgule
retournera un tableau (String[]
) de quatre éléments : Jean
, Louis
, David
et Roger,Virgule
.
Remarques :
@
.jcmsplugin.myplugin.name-list: @,Jean Louis David Roger,Virgule
La méthode appelée précédemment retournera cette fois un tableau de deux valeurs : Jean Louis David
et Virgule
.
Modifié le :
Publié le :
Si votre lanceur de projet eclipse est mal configuré, vous pouvez obtenir l'erreur suivante au démarrage de JCMS, pendant l'initialisation des dépendances des plugins :
09:59:34,749 FATAL [JCMS] [ChannelInitServlet] - An exception occured while initializing JCMS. The site is not available.
java.lang.NoClassDefFoundError: org/apache/lucene/analysis/standard/StandardAnalyzer
at java.lang.ClassLoader.defineClass1(Native Method)
...
Pour éviter cette erreur, vérifiez l'onglet Classpath de votre lanceur eclipse et supprimez votre projet des User Entries :
Modifié le :
Publié le :
Lorsque vous migrez un site eZ Publish 4.x vers 5.x sans utiliser l'assistant d'installation automatisée, vous devez générer la configuration Symfony de votre site.
EZ Publish 5 est maintenant un projet Symfony et utilise la gère sa configuration en fichiers .yml
.
Pour passer des anciens .ini
vers les nouveaux .yml
, utilisez la commande suivante
à la racine de votre projet :
php ezpublish/console ezpublish:configure --env=prod <group> <admin-siteaccess>
Remplacez <group>
par votre groupe de siteaccess (ex: mon_site
),
et <admin-siteaccess>
par le nom du siteaccess de votre back-office.
Remarques :
ezpublish/config/ezpublish_dev.yml
et ezpublish/config/ezpublish_prod.yml
.--env=prod
par --env=dev
pour utiliser la configuration de développement.Modifié le :
Publié le :
JCMS fournit un tag pour créer des liens : <jalios:link>
.
Pour que ce lien ouvre la cible dans une nouvelle fenêtre, il faut utiliser l'attribut htmlAttributes
du tag.
Par exemple :
<jalios:link data='<%= doc %>' htmlAttributes='target="_blank"' title='<%= glp("jcmsplugin.myplugin.target-blank.title") %>'>
<%= doc.getTitle(userLang) %>
</jalios:link>
Remarque :
Pour l’accessibilité, il est conseillé de préciser l'attribut title
pour informer
l'utilisateur de l'ouverture dans une nouvelle fenêtre.
Modifié le :
Publié le :
EZ Publish 5 utilise le principe des assets de Symfony. Les fichiers statiques (css, js, images, ...)
que vous utilisez dans vos bundles doivent donc être aussi présents dans le répertoire web/
.
De plus, tous les fichiers uploadés via le back-office (qui est encore en eZ Publish 4),
sont stockés par défaut dans le répertoire ezpublish_legacy/var/storage/
.
De la même manière, ils doivent aussi se retouver dans le répertoire web/
pour être servis par apache.
Pour mettre à jour votre répertoire web/
, vous avez le choix entre copier tous les fichiers statiques,
ou créer des liens symboliques.
Pour cela eZ Publish a surchargé la console de Symfony, et vous propose ces deux commandes (à lancer à la racine de votre projet) :
php ezpublish/console assets:install --symlink web
php ezpublish/console ezpublish:legacy:assets_install --symlink web
Explications :
web/
,
pointant vers les ressources des bundles.ezpublish_legacy/
.Remarque :
L'option --symlink web
est facultative. Si vous la retirer (ou si elle ne fonctionne pas), eZ Publish
créera des copies des fichiers au lieu des liens symboliques.
Modifié le :
Publié le :
Pour empêcher un module d'être chargé au démarrage de JCMS, modifiez son plugin.xml
.
Passez l'attribut initialize
de l'élément racine à false
.
Modifié le :
Publié le :
JCMS permet de simplifier la récupération d'une Data à partir de son id, stocké dans un fichier de propriétés.
Par exemple, pour stocker l'id d'une catégorie et la récupérer, vous feriez
jcmsplugin.myplugin.category.my-category.id: j_6540
et
String categoryID = channel.getProperty("jcmsplugin.myplugin.category.my-category.id");
Category category = channel.getCategory(categoryID);
Utilisez
$jcmsplugin.myplugin.category.my-category.id
et
Category category = channel.getCategory("$jcmsplugin.myplugin.category.my-category.id");
Explication :
Comme son nom commence par $
, JCMS sait qu'il s'agit d'une propriété contenant un id,
et peut directement récupérer la Data associée.
Modifié le :
Publié le :
L'extension ezjscore
d'eZ Publish permet d'appeler des fonctions PHP via des requêtes Ajax.
Vous pouvez l'utiliser pour mettre à jour une partie de la page sans la recharger complètement.
Le javascript va lancer une requête Ajax à la vue call
du module ezjscore
(et donc appeler l'URL /ezjscore/call
).
Cette vue va retourner le résultat d'une méthode PHP, en fonction de la configuration du fichier ezjscore.ini
.
Le résultat est alors disponible côté js et peut être utilisé pour modifier une partie de la page.
Les méthodes disponibles pour un appel Ajax doivent être implémentées dans des classes
héritant de ezjscServerFunctions
.
Par exemple dans le fichier monextension/classes/MyServerCallFunctions.php
:
<?php
/**
* Classe de fonctions à appeler en ajax.
*/
class MyServerCallFunctions extends ezjscServerFunctions {
/**
* Retourne le message "Hello x !", avec x le premier élément du tableau de paramètres,
* ou "world" si aucun paramètre n'est passé.
*
* @param array $args Arguments
* @return string
*/
public static function helloMessage( array $args ) {
// Log de l'appel de la fonction
eZLog::write( 'Appel Ajax : ' . __METHOD__, 'debug.log' );
if ( !empty( $args ) ) {
$message = 'Hello ' . $args[0] . ' !';
} else {
$message = 'Hello world !';
}
return $message;
}
}
Pour que votre classe soit utilisable vous devez la déclarer le fichier ezjscore.ini
:
Par exemple, dans le fichier monextension/settings/ezjscore.ini.append.php
:
<?php /* #?ini charset="utf-8"?
[ezjscServer]
# Liste des fonctions accessibles via des appels Ajax
FunctionList[]=my_function_name
[ezjscServer_my_function_name]
# Nom de la classe PHP
Class=MyServerCallFunctions
# Nom du fichier contenant la classe
File=extension/monextension/classes/MyServerCallFunctions.php
# Nom des fonctions proposées par la classe
Functions[]=my_function_name
*/ ?>
Remarque :
Une fois le fichier modifié, videz les caches et régénérez les autoloads, pour qu'eZ Publish trouve votre nouvelle classe.
Vous pouvez déjà appeler votre fonction en tapant l'url suivante dans votre navigateur :
http://monsite.com/index.php/ezjscore/call/my_function_name::helloMessage::dude
On appelle bien la vue call
du module ezjscore
, à laquelle on fournit le nom d'une fonction et
la liste des arguments, séparés par ::
.
Remarque :
Cela ne fonctionne que si vous êtes connecté en tant qu'administrateur.
Pour éviter ça, vous devez autorisez d'autres rôles à accéder à la vue ezjscore/call.
Vous pouvez même définir des limitations, pour n'autoriser que l'accès à la fonction my_function_name
.
Maintenant que votre fonction est accessible, voici comment l'utiliser dans vos template.
Tout d'abord, vous devez ajouter le code suivant à votre template, pour inclure l'API Javascript d'ezjscore :
{ezscript_require( array( 'ezjsc::jquery', 'ezjsc::jqueryio' ) )}
Remarques :
ezjsc::jquery
) est facultatif si vous avez déjà inclus
jQuery dans votre page, et peut même poser problème si la version de jQuery incluse est différente.Voici maintenant le code Javascript :
var dataSent = {arg0: 'dude', arg1: 'not_used'};
$.ez(
'my_function_name::helloMessage::dude::not_used',
dataSent,
function(data) {
// Si l'appel Ajax a retourné une erreur
if ( data.error_text ) {
console.error('Erreur : ' + data.error_text )
// Si l'appel Ajax a retourné des résultats
} else if ( data.content.length > 0 ) {
console.info('Résultat : ' + data.content);
}
}
);
Explications :
data.error_text
.data.content
.Modifié le :
Publié le :
Si vous utilisez phpMyAdmin pour sauvegarder une base de données, vous pouvez générer automatiquement le nom du fichier sql.
Vous pouvez définir par exemple un nom de la forme <nom_bdd>_<date>
. Pour cela, utilisez le modèle de nom suivant :
@DATABASE@_%Y-%m-%d
Pour la base devnotebook
, vous obtiendrez par exemple devnotebook_2013-02-14.sql
.
Modifié le :
Publié le :
EZ Publish fournit un grand nombre de fonctionnalités, accessibles dans les templates via des fetch (voir la documentation).
Vous pouvez créer vos propres fetch via le système de function_definition des modules.
module.ini
.Par exemple, dans le fichier monextension/settings/module.ini.append.php
:
<?php /* #?ini charset="utf-8"?
[ModuleSettings]
ExtensionRepositories[]=monextension
ModuleList[]=monmodule
Vous n'avez qu'un seul fichier à créer dans votre extension : modules/monmodule/function_definition.php
.
Ce fichier contient un tableau php qui liste les fonctions disponibles. Pour chacune d'elle, vous préciserez son nom, son type (lecture ou écriture), la méthode PHP à appeler et les paramètres à lui passer.
Par exemple :
<?php
$FunctionList = array();
$FunctionList['tree_unique'] = array(
'name' => 'tree_unique',
'operation_types' => array( 'read' ),
'call_method' => array(
'class' => 'ATContentFunctionCollection',
'method' => 'fetchObjectTree'
),
'parameter_type' => 'standard',
'parameters' => array(
array(
'name' => 'parent_node_id',
'type' => 'integer',
'required' => true
)
)
);
Explications :
tree_unique
lecture
(elle n'effectue pas de modifications sur les données, mais en retourne)fetchObjectTree()
de la classe ATContentFunctionCollection
Remarque :
Vous devez bien sur avoir créer une classe ATContentFunctionCollection possédant la méthode fetchObjectTree (). Le nom de la classe n'a pas d'importance, mais dans le cœur d'eZ Publish on ajoute FunctionCollection pour mettre en évidence les fonctionnalités utilisables dans les fetch.
Vous pouvez maintenant utiliser votre fetch après avoir vider les caches. Utilisez-le comme n'importe quel fetch natif :
{def $node_list = fetch(
'monextension', 'tree_unique',
hash( 'parent_node_id', $node.node_id )
)}
Modifié le :
Publié le :
Pour simplifier l'utilisation des fetch dans les templates, eZ Publish propose d'utiliser des alias.
Par exemple, si vous voulez compter récursivement les articles fils du nœud courant, le fetch standard serait :
{def $nb = fetch( 'content', 'tree_count',
hash(
'parent_node_id', $node.node_id,
'class_filter_type', 'include',
'class_filter_array', array( 'article' )
)
)}
Si vous utilisez souvent ce même fetch, vous aimerez sans doute lui créer un alias. La syntaxe devient alors :
{def $nb = fetch_alias(
'children_article_count',
hash( 'parent_node_id', $node.node_id )
)}
Explications :
children_article_count()
Pour informer eZ Publish de votre alias, il faut le déclarer dans le fichier fetchalias.ini
.
Par exemple, dans le fichier monextension/settings/fetchalias.ini.append.php
:
<?php /* #?ini charset="utf-8"?
[children_article_count]
# Compte récursivement le nombre d'articles sous le nœud dont l'ID est en paramètre
Module=content
FunctionName=tree_count
Constant[class_filter_type]=include
Constant[class_filter_array]=article
Parameter[parent_node_id]=parent_node_id
*/ ?>
Explications :
children_article_count
) est le nom de votre alias, à utiliser dans votre template.Remarques :
settings/fetchalias.ini
.Modifié le :
Publié le :
Par défaut, le pare-feu bloque les ports utilisés par Apache, à savoir 80
et 443
.
Pour autoriser une machine distante à se connecter à Apache, ajoutez les lignes suivantes au
début du fichier /etc/sysconfig/iptables
:
-A RH-Firewall-1-INPUT -p tcp -m state --state NEW -m tcp --dport 80 -j ACCEPT
-A RH-Firewall-1-INPUT -p tcp -m state --state NEW -m tcp --dport 443 -j ACCEPT
Redémarrez maintenant le pare-feu pour qu'il prenne en compte la modification :
service iptables restart
Modifié le :
Publié le :
Pour autoriser une machine distante à se connecter à votre base de données MySQL, exécutez la requête suivante :
GRANT ALL privileges ON my_db.* TO my_user@my_server IDENTIFIED BY 'my_password';
Explications :
my_db
.Remarque :
Si my_user
doit être super-utilisateur, vous devez ajouter WITH GRANT OPTION
à la requête :
GRANT ALL privileges ON my_db.* TO my_user@my_server IDENTIFIED BY 'my_password' WITH GRANT OPTION;
Modifié le :
Publié le :
Sous Linux, pour connaitre les modules chargés par Apache, utilisez la commande :
/usr/local/apache/bin/httpd -M
Remarque :
Selon votre distribution, remplacez httpd
par apache
ou apachectl
.
Modifié le :
Publié le :
Lorsque vous lancez Apache sous Linux, via une commande du type :
/sbin/service apache start
Vous pouvez obtenir l'erreur suivante :
Syntax error on line xxx of /usr/local/apache2/conf/httpd.conf: Cannot load /usr/local/apache2/modules/libphp5.so into server: /usr/local/apache2/modules/libphp5.so: cannot restore segment prot after reloc: Permission denied
Pour corriger l'erreur, exécutez la commande suivante :
chcon -t textrel_shlib_t /usr/local/apache2/modules/libphp5.so
Modifié le :
Publié le :
Il est utile pour simplifier le débogage, d'utiliser les assertions d'un langage, que ce soit en Java ou en PHP. Les assertions ne s'activent qu'en mode développement, et seront tout simplement ignorées en production (= pas de perte de performance).
Voici les deux cas dans lesquels utiliser les assertions :
Tutoriel pour utiliser les assertions avec PHP : http://openclassrooms.com/courses/les-assertions-en-php.
Modifié le :
Publié le :
Tout d'abord, voici comment récupérer cet EntityManager, depuis un Contrôleur :
// Récupération de l'entity manager
$entityManager = $this->getDoctrine()->getManager();
Voici les principales méthodes de l'EntityManager (voir toutes les méthodes).
Cette méthode signale à Doctrine que l'objet doit être enregistré. Elle ne doit être utilisée que pour un nouvel objet et non pas pour une mise à jour.
Ex :
// Crée l'article et le signale à Doctrine.
$article1 = new Article;
$article1->setTitre('Mon dernier weekend');
$entityManager->persist($article);
Met à jour la base à partir des objets signalés à Doctrine. Tant qu'elle n'est pas appellée, rien n'est modifié en base.
Ex :
// Crée l'article en base et met à jour toutes les entités persistées modifiées.
$entityManager->persist($article);
$entityManager->flush();
Annule tous les persist()
en cours.
Si le nom d'une entité est précisé (son namespace complet ou son raccourci),
seuls les persist()
sur les entités de ce type seront annulés.
Ex :
$entityManager->persist($article);
$entityManager->persist($commentaire);
$entityManager->clear();
// N'exécutera rien, car les deux persists sont annulés par le clear
$entityManager->flush();
Annule le persist() effectué sur l'entité en argument.
Au prochain flush()
, aucun changement ne sera donc appliqué à l'entité.
Ex :
$entityManager->persist($article);
$entityManager->persist($commentaire);
$entityManager->detach($article);
// Enregistre $commentaire mais pas $article
$entityManager->flush();
Retourne true
si l'entité donnée en argument est gérée par l'EntityManager
(= s'il y a eu un persist()
sur l'entité).
Ex :
$entityManager->persist($article);
var_dump($entityManager->contains($article)); // Affiche true
var_dump($entityManager->contains($commentaire)); // Affiche false
Rafraîchit l'entité donnée en argument pour la mettre dans l'état où elle se trouve en base de données.
Cela écrase et annule donc tous les changements qu'il a pu y avoir sur l'entité depuis le dernier flush()
.
Ex :
$article->setTitre('Un nouveau titre');
$entityManager->refresh($article);
var_dump($article->getTitre()); // Affiche « Un ancien titre »
Signale à Doctrine qu'on veut supprimer l'entité en argument de la base de données.
Effectif au prochain flush()
.
Ex :
$entityManager->remove($article);
// Exécute un DELETE sur $article
$entityManager->flush();
Modifié le :
Publié le :
Voici un récapitulatif des commandes de base de la console de Symfony pour Doctrine :
php bin/console doctrine:database:create
php bin/console doctrine:schema:update --dump-sql
php bin/console doctrine:schema:update --force
La première commande permet de visualiser les requêtes à exécuter. La seconde les exécute.
php bin/console generate:doctrine:entity
Renseignez ensuite le nom de l'entité avec le nom du bundle devant (ex: BlogBundle:Article
),
le format de configuration à utiliser, les éventuels champs à créer,
et si vous souhaitez également générer le code du repository.
php bin/console doctrine:generate:entities BlogBundle:Article
À adapter en fonction de l'entité à mettre à jour.
Modifié le :
Publié le :
Sous Windows, lorsque vous essayez d'installer les dépendances de Symfony avec Composer, vous pouvez obtenir l'erreur suivante :
Loading composer repositories with package information
Updating dependencies
Nothing to install or update
Generating autoload files
Script Sensio\Bundle\DistributionBundle\Composer\ScriptHandler::buildBootstrap h
andling the post-update-cmd event terminated with an exception
[RuntimeException]
An error occurred when generating the bootstrap file.
Exécutez alors les commandes suivantes :
composer update --no-scripts
php vendor/sensio/distribution-bundle/Sensio/Bundle/DistributionBundle/Resources/bin/build_bootstrap.php
Modifié le :
Publié le :
Sous Windows, lorsque vous essayer d'installer des dépendances avec Composer (le gestionnaire de dépendances de PHP), vous pouvez obtenir l'erreur suivante :
Loading composer repositories with package information
Installing dependencies
- Installing twig/extensions (dev-master 5c2d515)
Cloning 5c2d515d4624bdd588226d688173cf0399a4d8cf
[RuntimeException]
Failed to execute git checkout "5c2d515d4624bdd588226d688173cf0399a4d8cf" &
& git reset --hard "5c2d515d4624bdd588226d688173cf0399a4d8cf"
fatal: Not a git repository (or any of the parent directories): .git
Remplacez alors la commande :
composer install
par :
composer update --prefer-dist
Modifié le :
Publié le :
WampServer utilise deux fichiers php.ini
(adaptez les chemins en fonction du dossier racine de WampServer et des versions d'apache et PHP) :
D:\Chemin\vers\wamp\bin\php\php5.3.13\php.ini
D:\Chemin\vers\wamp\bin\apache\apache2.2.22\bin\php.ini
Si vous utilisez PHP en ligne de commande, celui-ci utilisera le premier php.ini : celui qui se trouve à la racine du répertoire de PHP.
Si vous utilisez PHP via votre navigateur, le second php.ini sera utilisé : celui qui se trouve dans l'arborescence d'Apache.
Modifié le :
Publié le :
Pour créer un Bundle avec Symfony, utilisez la commande :
php app/console generate:bundle
MonBundle
).
Si vous avez mis oui à l'étape précédente, préfixez le d'un namespace (ex: MonSite\MonBundle
).src/
inchangé.Si vous préférez le créer à la main, voici ce qu'il faut retenir pour le rendre opérationnel :
src/MonBundle/
MonBundle.php
.new MonBundle\MonBundle(),
dans AppKernel.php
.app/config.routing.yml
:mon_bundle:
resource: "@MonBundle/Controller/"
type: annotation
prefix: /
app/config.yml
:
imports:
# [...]
- { resource: "@MonBundle/Resources/config/services.yml" }
Modifié le :
Publié le :
Lorsque vous utilisez des animations en Javascript avec jQuery, il arrive que les animations s'emmêlent si elles sont exécutées rapidement les unes après les autres.
Par exemple, si vous construisez un menu en accordéon et que vous cliquez très vite de lien en lien, les menus n'ont pas forcément le temps de se déplier puis se replier.
Vous pouvez remédier à ce problème en utilisant la fonction stop()
de jQuery, pour que les animations en
cours s'arrêtent avant d'en commencer de nouvelles.
Par exemple :
// Arrêt de toutes les animations en cours sur les éléments du menus
jQuery('#right-menu li.sub-menu-container ul.sub-menu').stop(true, true);
Toutes les animations en cours sur les éléments ul.submenu
sont annulées/s'arrêtent instantanément.
Remarque :
Le second paramètre passé à true
, permet de terminer les animations en cours instantanément.
S'il est à false
, les animations se terminent normalement mais toutes celles en attente sont annulées.
Modifié le :
Publié le :
JCMS intègre un framework pour utiliser de l'AJAX dans les pages d'un site.
Pour recharger un élément du DOM à partir d'une jsp, voici le code à utiliser :
var id = 'mon_element';
var url = 'plugins/MonPlugin/jsp/ajax/maJsp.jsp';
var value = $(id).value;
var params = 'key=' + value;
// Rechargement de l'élément
JCMS.ajax.Refresh._refresh(id, url, {
history: false,
formParams: params
});
params
est la liste des paramètres à ajouter à l'url, qui pourront être récupérés dans la jsp appelée :String keyValue = request.getParameter("key");
Modifié le :
Publié le :
EZ Publish fournit la classe eZSys
, propose pas mal de méthodes pour récupérer par exemple :
Cette classe se trouve dans lib/ezutils/classes/ezsys.php
.
Modifié le :
Publié le :
Si vous avez besoin d'un système de chat dans votre site eZ Publish, voici une extension qui peut faire l'affaire : eZ phpFreeChat.
Elle fournit un module pour eZ Publish, dont l'unique vue affiche un chat. Le chat est composé d'une discussion principale visible par tous les utilisateurs connectés au chat et permet des discussions privées entre deux utilisateurs (en cliquant sur leur nom).
Remarque :
L'extension utilise la version 1.3 de phpfreechat, qui existe maintenant en 2.1.1.
Modifié le :
Publié le :
Si vous n'utilisez pas les fichiers .less
dans vos modules et que vous ne modifiez pas ceux de JCMS,
il est inutile de les recompiler à chaque redémarrage, car les fichiers .css
générés ne changeront pas.
Pour éviter cette compilation automatique, ajoutez cette propriété au custom.prop
:
channel.less-compile.startup: false
Modifié le :
Publié le :
EZ Publish fournit par défaut 3 classes de contenus pour créer des forums :
Forum, Forum topic et Forum reply.
L'extension ezwebin
fournit les templates associés et vous pouvez ainsi créer des forums,
des conversations et des messages.
Malgré cela, il manque beaucoup de fonctionnalités courantes attendues sur un forum : une messagerie privée, un suivi des discussions, des statistiques, du BBCode, un affichage en tableau des forums et des sujets, une gestion de rangs, de la modération, la possibilité de signaler un abus, ...
Ces fonctionnalités sont implémentées par l'extension xrowForum. Elle fournit :
L'extension distribuée sur le repo svn est mal internationalisée et mal traduite. Voici la même version corrigée.
Modifié le :
Publié le :
Voici la configuration à utiliser pour développer un site eZ Publish avec WampServer. Elle s'articule autour de 3 fichiers de configuration :
httpd.conf
: Fichier de configuration du serveur Apachehttpd-vhosts.conf
: Fichier de configuration apache pour les hôtes virtuelsphp.ini
: Configuration de phpRemarque :
Dans cet exemple, la version 2.2 64 bits de WampServer est utilisée, avec Apache 2.2.22 et PHP 5.3.13.
D:\Dev\php_projects\MonSite\
D:\Dev\wamp\
Ce fichier se trouve dans bin\apache\apache2.2.22\conf\
, à partir de la racine de WampServer.
Tout en bas du fichier, activez les hôtes virtuels en décommentant cette ligne :
Include conf/extra/httpd-vhosts.conf
Voici un exemple du fichier httpd.conf.
Via l'interface de Wamp, activez les modules Apache suivants :
Éditez le fichier host (C:\Windows\System32\drivers\etc\hosts
) en tant qu'administrateur.
Pour pouvoir l'enregistrer avec Notepad++, lancez l'éditeur en tant qu'administrateur
(Clic-droit sur l'exécutable) et ouvrez ensuite le fichier host
.
Ajoutez la ligne
127.0.0.1 mon_site.loc
Lorsque vous utiliserez l'adresse mon_site.loc
, votre navigateur saura ainsi qu'il s'agit de votre
ordinateur et non pas d'une machine sur internet.
Modifiez maintenant le fichier httpd-vhosts.conf
(ex : D:\Dev\wamp\bin\apache\apache2.2.22\conf\extra\httpd-vhosts.conf
)
en ajoutant les lignes :
<VirtualHost *:80>
ServerName mon_site.loc
DocumentRoot D:/Dev/php_projects/MonSite
<Directory D:/Dev/php_projects/MonSite>
Options Indexes FollowSymLinks MultiViews
Order allow,deny
allow from all
</Directory>
</VirtualHost>
Voici un exemple du fichier httpd-vhosts.conf.
WampServer utilise deux fichiers php.ini
différents. Le premier est utilisé par votre eZ Publish ou
n'importe quel site servi par votre serveur Apache.
Le second est utilisé lorsque vous appelez PHP via l'invite de commande.
Voici où les trouver depuis la racine de votre dossier WampServer :
bin\apache\apache2.2.22\bin\php.ini
bin\php\php5.3.13\php.ini
Le plus simple est d'utiliser la même configuration entre les deux lorsque vous êtes en phase de développement.
Voici les propriétés à éditer :
# Temps maximum d'exécution de script (en s)
max_execution_time = 480
# Temps maximum pour uploader un fichier
max_input_time = 180
# Taille maximale des fichiers uploadables
post_max_size = 8M
# Zone de date
date.timezone = "Europe/Paris"
Voici un exemple du fichier php.ini.
Remarque :
Vous pouvez également activer XDebug pour pouvoir déboguer de manière optimale.
Comme pour les modules Apache, activez ces extensions PHP via l'interface de WampServer :
Modifié le :
Publié le :
Lorsque vous lancer une application JCMS avec tomcat 1.5, vous pouvez avoir cette erreur :
java.lang.NoClassDefFoundError: com/sun/tools/javac/Main
Elle signifie qu'il manque l'archive tools.jar
au classpath.
Pour l'ajouter à votre projet au lancement de l'application, effectuez ces quelques étapes :
tools.jar
qui se trouve dans le répertoire lib/
de votre jdk (ex: D:\Dev\DevPack\jdk\lib\tools.jar
)Modifié le :
Publié le :
Pour ajouter une variable d'environnement ou en modifier la valeur, utilisez la commande :
export INSTALL4J_JAVA_HOME='/var/lib/jdk1.6.0_33'
en remplaçant INSTALL4J_JAVA_HOME
par le nom de la variable à définir et en modifiant la valeur entre 'quotes'.
Pour que cette variable soit définie automatiquement lorsque vous utilisez la console, il faut modifier le fichier
.bashrc
dans le dossier home de l'utilisateur pouvant utiliser cette variable.
Pour l'utilisateur root
par exemple, il s'agit du fichier /root/.bashrc
.
Dans ce fichier, ajoutez la même commande :
export INSTALL4J_JAVA_HOME='/var/lib/jdk1.6.0_33'
Modifié le :
Publié le :
Lorsque vous affichez une publication en Full display, le portail d'affichage choisi sera celui de la première catégorie à laquelle est est rattachée. Si cette catégorie n'a pas de portail qui lui est propre, celui de sa catégorie parente est utilisée, et ainsi de suite jusqu'à trouver un portail d'affichage.
Si le contenu n'est pas catégorisé, le portail d'affichage par défaut sera utilisé.
Pour forcer une publication à s'afficher dans un portail spécifique, on peut ajouter un paramètre à l'url :
&portal=<portal_id>
.
Avec un tag jalios:link
on obtient donc quelque chose comme :
<jalios:link data='<%= publication%>' paramNames='<%= paramNames %>' paramValues='<%= paramValues %>'>
Explication :
paramNames
: Tableau de String contenant les paramètres à ajouter à l'url (ici: {"portal"}
)paramValues
: Valeurs des paramètres du premier tableau, dans l'ordre correspondant
(ici: {channel.getProperty("jcmsplugin.portail.full-display.id")}
)Remarque :
JCMS 7.1 comporte un bug à propos de ce tag, et les paramètres paramsNames
et paramValues
ne fonctionnent pas. On doit alors utiliser le paramètre params
:
<jalios:link data='<%= publication %>' params='<%= "portal=" + channel.getProperty("jcmsplugin.portail.full-display.detail.id") %>'>
Ce paramètre reçoit une valeur de la forme <paramName>=<paramValue>
.
Il remplace tous les autres paramètres transmis initialement dans l'url et est donc à éviter
lorsque paraNames
et paramValues
fonctionnent.
Modifié le :
Publié le :
S'il vous arrive d’utiliser l’outil de ligne de commande DOS sous Windows, vous savez que par défaut,
celui-ci se positionne dans votre répertoire personnel.
C'est assez pénible car à chaque fois, vous êtes obligé de faire plein de cd machin
pour arriver au répertoire de votre choix.
Pour éviter de refaire cette manip à chaque fois (surtout si c'est pour se rendre toujours au même endroit), vous pouvez modifier le chemin utilisé par défaut.
Il suffit d’aller dans la base de registre (Démarrer > Exécuter... > regedit) et de naviguer
jusqu’à la clé HKEY_CURRENT_USER \ Software \ Microsoft \ Command Processor
.
Créez ensuite une nouvelle valeur chaîne dans la zone de droite et nommez-la Autorun
.
Modifiez ensuite cette clé et donnez-lui la valeur suivante en remplaçant bien sûr mon\chemin
par celui de votre choix :
cd /d d:\mon\chemin
Et voilà, les changements s'appliquent instantanément sans besoin de redémarrer. (Relancez tout de même votre invite de commande si elle était déjà ouverte.)
Remarque importante :
Cette manip semble poser problème dans certains scripts batch utilisant %CD%
.
Rendez-vous dans le répertoire où vous souhaitez ouvrir votre invite de commande, via l'explorateur Windows.
Faites Shift
+ Clic droit
et choisissez Ouvrir un invite de commande ici. Et voilà c'est fait.
Cette solution est beaucoup moins intrusive et plus simple mais vous devez refaire la manip à chaque fois.
Modifié le :
Publié le :
Vous pouvez modifier l'emplacement où les contenus sont créés par défaut, selon leur classe de contenu.
Ainsi, eZ Publish définit par exemple qu'un contenu Image doit être placé dans medias/images
et
qu'un contenu Fichier dans medias/files
.
Si vous renommez ces emplacements (ex: Fichiers
à la place de Files
) et
lancez le cronjob de régénération des urls, medias/files
devient medias/fichiers
.
Si vous ne modifiez pas la configuration, eZ Publish ne trouve plus l'emplacement et importe les fichiers à la racine.
Vous devez donc surcharger le fichier content.ini
en réécrivant les emplacements déjà existants et
surtout en modifiant ceux que vous avez renommés.
Par exemple, dans le fichier monextension/settings/content.ini.append.php
:
<?php /* #?ini charset="utf-8"?
[RelationAssignmentSettings]
ClassSpecificAssignment[]
ClassSpecificAssignment[]=user,user_group;utilisateurs/membres
ClassSpecificAssignment[]=image;medias/images
ClassSpecificAssignment[]=video;medias/multimedia
ClassSpecificAssignment[]=file;medias/fichiers
ClassSpecificAssignment[]=quicktime;medias/multimedia
ClassSpecificAssignment[]=windows_media;medias/multimedia
ClassSpecificAssignment[]=real_video;medias/multimedia
ClassSpecificAssignment[]=flash;medias/multimedia
*/ ?>
Modifié le :
Publié le :
Cet article explique comment démarrer un nouveau projet JCMS 7.X avec un développement en modules synchronisés.
D:\Dev
).eclipse.bat
.Il faut préciser à eclipse quelles classes compiler et avec quelles bibliothèques.
src/
existant, et ajoutez le dossier WEB-INF/classes/
.Tomcat6
et validez.tomcat6/lib/
de votre Devpack.ecj-3.7.jar
, el-api.jar
, jasper.jar
, jasper-el.jar
,
jsp-api.jar
, servlet-api.jar
et validez.WEB-INF/lib/
de JCMS.Pour pouvoir lancer l'application, vous devez indiquer le paramétrage de tomcat et les configurations d'exécution d'eclipse.
MonProjet
par le nom de votre projet,
et D:/Dev/DevPack/
par le chemin vers votre devpack.jdk16/
et tomcat6/
ne se trouvent pas à la racine
de votre devpack, modifiez les chemins correspondants en conséquence.tomcat.xml
à la racine de votre projet en adaptant au besoin le chemin
vers la racine de votre application et le contexte (/jcms
par défaut). Exemple :<Context path="/jcms" docBase="D:/Dev/workspaces/MonProjet" debug="0"></Context>
/jcms
étant le contexte choisi dans le tomcat.xml
).Modifié le :
Publié le :
Dans eZ Publish on utilise tout le temps les fichiers de configuration .ini
.
Il y en a un peu partout dans l'arborescence d'un projet eZ Publish et il est donc facile de s'y perdre.
settings/
à la racine du projet ou dans ceux des extensions.Les fichiers .ini
sont les fichiers par défaut, fournis par eZ Publish et par les extensions.
Lorsque vous surchargez ces fichiers, utilisez les fichiers .ini.append.php
(ex: site.ini.append.php
).
Si dans votre extension, vous avez besoin de créer un nouveau fichier de configuration,
utilisez donc l'extension .ini
.
Si vous distribuer votre extension et que la personne qui l'utilise veur surcharger une propriété,
elle créera elle, un fichier .ini.append.php
.
Voici l'ordre de prise en compte des fichiers .ini
(et .ini.append.php
),
du moins important au plus important. Les chemins sont relatifs à la racine de l'application.
settings/
: Vous y trouverez tous les fichiers de configuration par défaut.
Ils impactent indifféremment tous les siteaccess. Ces fichiers ne doivent pas être modifiés !extension/mon_extension/settings/siteaccess/mon_siteaccess/
:
Seule la configuration du siteaccess mon_siteaccess
sera impactée.settings/siteaccess/mon_siteaccess/
: Idem, seule la configuration du siteaccess mon_siteaccess
sera impactée.extension/mon_extension/settings/
:
Le cas le plus couramment utilisé. Vous modifier ici toutes les configurations que vous voulez,
pour tous les siteaccess.settings/override/
: C'est le niveau le plus haut. Tout ce qui est ici sera pris en compte en priorité,
quel que que soit le siteaccess.L'ordre d'activation des extensions dans le fichier site.ini
a une importance.
La première extension déclarée surcharge la deuxième, qui surcharge la troisième et ainsi de suite.
Modifié le :
Publié le :
Lorsque vous affichez une Google Map avec beaucoup de points rapprochés, ils risquent de se chevaucher. Pour éviter ce problème, vous pouvez les regrouper en utiliser le MarkerClustering.
L'autre avantage est le temps de chargement de la carte. Puisqu'il y a des regroupements, il y a moins de points à afficher.
Voici un exemple :
À gauche une carte avec beaucoup de points et à droite, la même avec des regroupements.
Pour utiliser le regroupement, il suffit de créer la carte et les points,
mettre les points dans un tableau et créer un nouvel objet MarkerClusterer
.
Voici un aperçu du code à ajouter pour créer des points regroupés :
// Soit data un tableau d'objets ayant des latitudes et longitudes
var markers = [];
for (var i = 0; i < 100; ++i) {
var marker = new google.maps.Marker({
"lat": data[i].lat,
"long": data[i].long
});
markers.push(marker);
}
var markerCluster = new MarkerClusterer(map, markers);
Remarque :
Il faut également ajouter le fichier markerclusterer.js
à votre page.
Modifié le :
Publié le :
Il est arrivé que vous ayez une table avec deux champs indépendants, ne devant pas être remplis simultanément.
Par exemple pour un utilisateur : s'il est étudiant le champ formation est rempli et s'il travaille c'est le champ métier. Pour simplifier l'utilisation de ces données, vous voulez pouvoir récupérer un champ activité qui contient soit la formation, soit le métier.
En SQL, vous pouvez donc utiliser la fonction COALESCE()
:
SELECT COALESCE( formation, metier ) AS activite FROM user;
Parmi les noms de champ en paramètres, la fonction COALESCE()
retourne le premier non nul.
Modifié le :
Publié le :
Pour connaitre le contenu d'une variable dans eZ Publish il existe plusieurs pour faciliter le débogage.
Cette fonction couplée avec les balises <pre>
, affiche le contenu de la variable de manière
récursive et structurée.
echo '<pre>';
print_r( $variable, false);
echo '</pre>';
affichera
Cette fonction affiche la même chose que print_r()
, mais de manière typée.
Contrairement à l'autre, elle affiche aussi les valeurs null
ou false
.
echo '<pre>';
var_dump( $variable );
echo '</pre>';
affichera
Cette fonction affiche la même chose que la fonction var_dump()
,
mais dans la partie debug de bas de page (si le mode debug est activé).
eZDebug::writeDebug( $variable );
affichera
Cet opérateur affiche le contenu de tous les attributs de la variable, si celle-ci est un objet ou un tableau. Le second paramètre définit le niveau de profondeur à afficher.
{$variable|attribute( 'show', 1 )}
affichera
{$variable|attribute( 'show', 2 )}
affichera
Cette fonction fait la même chose que eZDebug::writeDebug()
, mais côté template.
{debug-log var=$variable}
affichera
Modifié le :
Publié le :
Lorsque vous développez en PHP, déboguer avec une interface graphique peut vous faire gagner beaucoup de temps. Les IDE comme Eclipse ou NetBeans proposent ce genre d'interface pour Java, mais également pour PHP.
Grâce à eux, vous pouvez mettre des points d’arrêt, exécuter le programme pas à pas et voir l’évolution des valeurs des variables en temps réel, à chaque étape de l'exécution du script.
Pour vérifier l’installation de XDebug sur votre serveur, créez un page PHP
contenant l'appel à la méthode phpinfo()
.
Une page de la forme suivante devrait apparaître :
Plus bas dans la page, l'encadré suivant devrait apparaître si XDebug est activé :
XDebug est installé par défaut sur WampServer, mais n’est pas configuré en mode remote (débogage à distance). Pour modifier la configuration de Xdebug :
php.ini
(icône Wampserver > PHP > php.ini)xdebug
du fichier :[xdebug]
xdebug.remote_enable=on
xdebug.remote_host="127.0.0.1"
xdebug.remote_port=9000
xdebug.remote_handler="dbgp"
xdebug.remote_mode=req
sudo apt-get install php5-xdebug
sudo gedit /etc/php5/conf.d/xdebug.ini
[xdebug]
xdebug.remote_enable=on
xdebug.remote_host="127.0.0.1"
xdebug.remote_port=9000
xdebug.remote_handler="dbgp"
xdebug.remote_mode=req
sudo /etc/init.d/apache2 restart
Pour simplifier le démarrage d’une session XDebug, il existe l'extension easy Xdebug
pour Firefox.
Une fois installée, deux icônes s’affichent dans la barre d'outils.
Lorsque le débogage n'est pas actif, cliquez sur
pour l'activer. Pour le désactiver, cliquez sur
Il s’agit maintenant d’indiquer à Eclipse que vous souhaitez utiliser XDebug pour déboguer votre projet :
http://monsite.com
)index.php
)Pour activer le débogueur, ouvrez NetBeans et cliquez sur Debug Project (Ctrl + F5). Dans votre navigateur, cliquez ensuite sur le bouton activer le débogage de l'extension easy Xdebug (sous Firefox).
Remarque :
Si vous utilisez un autre navigateur que Firefox, vous pouvez activer le débogage
en ajoutant ?XDEBUG_SESSION_START=netbeans-xdebug
à la fin de votre URL.
Un cookie sera ainsi créé et repéré par NetBeans, pour utiliser le débogage.
Modifié le :
Publié le :
Pour créer un lien de téléchargement vers le fichier d'un contenu de type File, utilisez :
{concat(
'content/download/', $object.data_map.file.contentobject_id,
'/', $object.data_map.file.id,
'/version/', $object.data_map.file.version ,
'/file/', $object.data_map.file.content.original_filename|urlencode()
)|ezurl( 'no' )}
Explication :
La variable $object
doit être un objet de contenu, soit une instance de eZContentObject
.
Modifié le :
Publié le :
Voici une extension eZ Publish permettant de récupérer des webservices fournis par Play! : AT Play Connector.
Cette extension propose une application Play! d'exemple, correspondant aux exemples des articles précédents.
Pour chaque service web de cette application, une méthode PHP et un fetch()
permettent de récupérer son résultat côté eZ Publish.
Ces méthodes sont implémentées dans le fichier classes/ATWSFunctionCollection.php
(à partir de la racine de l'extension), dont voici un extrait :
<?php
class ATWSFunctionCollection extends ATWebServices {
/**
* Appel du service web /test
*
* @param string $nom Nom des locataires recherchés
* @param string $dateNaissance Date de naissance des locataires recherchés
* @param string $lang Langue des résultats
*/
public static function test() {
// Nom du service web
$wsName = '/test';
// Construction de la requête
$wsRequest = $wsName;
return array( 'result' => parent::getJSONResult( $wsRequest ) );
}
/**
* Appel du service web /helloWorld
*
* @param string $name Nom de la personne à saluer
*/
public static function helloWorld( $name ) {
// Nom du service web
$wsName = '/helloWorld';
// Construction de la requête avec les paramètres
$wsRequest = $wsName . '?';
if ( $name != null ) {
$wsRequest .= 'name=' . urlencode( $name );
}
return array( 'result' => parent::getJSONResult( $wsRequest ) );
}
}
Explications :
ATWebservices
également fournie par l'extension.getJSONResult()
de la classe ATWebServices
.Le fichier modules/atplayws/function_definition.php
(à partir de la racine de l'extension)
déclare des fetch()
utilisables depuis les templates :
<?php
$FunctionList = array( );
$FunctionList['test'] = array(
'name' => 'test',
'operation_types' => array( 'read' ),
'call_method' => array(
'class' => 'ATWSFunctionCollection',
'method' => 'test' ),
'parameter_type' => 'standard',
'parameters' => array( )
);
$FunctionList['hello_world'] = array(
'name' => 'hello_world',
'operation_types' => array( 'read' ),
'call_method' => array(
'class' => 'ATWSFunctionCollection',
'method' => 'helloWorld' ),
'parameter_type' => 'standard',
'parameters' => array(
array( 'name' => 'name',
'type' => 'string',
'required' => true ),
)
);
Explications :
Pour chaque fetch, on déclare son nom, la méthode PHP correspondante à appeler, ainsi que les paramètres à lui fournir.
Remarque :
Pour plus de détails sur l'utilisation de l'extension, consultez le fichier README.txt
présent à sa racine.
Modifié le :
Publié le :
Tout d'abord, vous avez besoin d'un JDK 6 ou supérieur pour pouvoir utiliser Play! en Java.
export PATH=$PATH:/path/to/play-2.0.3
.
Si vous êtes sous Windows, modifiez les variables d'environnement.play help
Si vous utilisez eclipse, Netbeans, ou un autre IDE pour Java possédant un workspace, il est préférable de créer votre application dans ce workspace.
Play! peut créer l'application pour vous, via la commande :
play new chemin/vers/le/workspace/nom_application
PlayWS
)simple Java application
Pour utiliser Play! avec eclipse, il faut "eclipsifier" votre application. Pour cela, utilisez les commandes suivantes :
cd chemin/vers/le/workspace/nom_application
play
eclipsify
:$ eclipsify
Remarque :
Si vous obtenez une erreur pendant la commande eclipsify ressemblant à class file has wrong version 50.0, should be 49.0
, c'est que vous devez mettre à jour votre JDK ou modifier vos variables d'environnement.
Modifié le :
Publié le :
Play! vous permet de créer des services web accessibles via de simples requêtes HTTP.
Vous devez pour cela définir des routes, pour que l'application sache quelle méthode exécuter en fonction de la requête HTTP.
C'est le rôle du fichier conf/route
, dont voici un exemple :
# Routes
# Ce fichier définit les différentes routes (avec les routes prioritaires en premier)
# ~~~~
# Mappe les fichiers de ressources statiques du répertoire /public avec le chemin /assets dans l'URL
GET /assets/*file controllers.Assets.at(path="/public", file)
GET /test controllers.Application.test()
GET /helloYou controllers.Application.helloYou(firstName, lastName)
GET /userList controllers.Application.userList()
Explications :
Pour chaque service web, on définit une route de la forme :
<Type de requête (GET|POST)> <url> <package.Classe.method()>
.
L'appel de l'URL http://localhost:9000/test
retournera le résultat de la méthode test()
de la classe Application
du package controllers
.
La méthode helloYou()
nécessite les arguments firstName
et lastName
.
Pour l'appeler, l'url sera http://localhost:9000/helloYou?firstName=Jean-Louis&lastName=David
.
Comme la requête est définie en GET
, il suffit d'ajouter les paramètres dans l'url sous
la forme nom_param=valeur
.
Remarque :
La première route (GET /assets/*file...
) est un peu particulière et sert au
fonctionnement interne de Play!, pour gérer les fichiers statiques de l'application.
Voici un aperçu de la classe Java qui contient les méthodes définie dans le fichier route :
package controllers;
import java.util.Date;
import models.WSResult;
import models.beans.Bean;
import models.beans.MessageBean;
import play.libs.Json;
import play.mvc.Controller;
import play.mvc.Result;
public class Application extends Controller {
public static Result test() {
WSResult result = new WSResult();
result.addBean(new Bean() {
public String message = "Test";
public Date date = new Date();
});
return ok(Json.toJson(result));
}
public static Result helloYou(String firstName, String lastName) {
WSResult result;
if (!firstName.equals("") && !lastName.equals("")) {
result = new WSResult();
result.addBean(new MessageBean("Hello " + firstName + " " + lastName + "!"));
} else {
result = new WSResult("Paramètres incorrectes.", WSResult.ERROR_CODE_CUSTOM_PARAMS);
}
return ok(Json.toJson(result));
}
}
Explications :
Application
étend la classe Controller
fournie par Play!.route
, on retrouve bien la méthode dans la
classe Application
, avec ses arguments. (Notez que les noms des arguments doivent être identiques.)Result
(classe également fournie par Play!).Result
est associée une liste d'objets Bean
.Bean
regroupe des informations à rendre accessible via un service web
(un message, le résultat d'un requête SQL, ...)Result
a été étendue par la classe WSResult
,
pour lui ajouter une liste d'objets Bean
, un éventuel message d'erreur, et d'autres informations.ok(Json.toJson(result))
fournie par Play!.Normalement, un Bean représente une ligne de données provenant d'une source (base de données, fichier, ...).
C'est une classe Java des plus simples, avec juste des attributs et leurs getter()
et setter()
.
Imaginons par exemple qu'on veuille représenter un utilisateur de site web présent dans une table de base de données, on pourrait avoir :
package models.beans;
public class UserBean extends Bean {
private int userID;
private String email;
private String login;
private String password;
public static final String FIELD_USER_ID = "user_id";
public static final String FIELD_EMAIL = "email";
public static final String FIELD_LOGIN = "login";
public static final String FIELD_PASSWORD = "password";
public int getUserID() {
return userID;
}
public void setUserID(int userID) {
this.userID= userID;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getLogin() {
return login;
}
public void setLogin(String login) {
this.login = login;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
Explications :
Bean
, car la classe WSResult
attend des objets Bean
dans sa liste.Une fois que la classe UserBean
a été créée, il faut écrire dans la classe Application
la méthode
qui va consulter la base de données et retourner un objet Result :
package controllers;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import models.WSResult;
import models.beans.UserBean;
import play.libs.Json;
import play.mvc.Result;
public class Application extends ApplicationController {
/**
* Retourne la liste des utilisateurs de la base de données.
*
* @return Result
*/
public static Result userList() {
WSResult result = new WSResult();
String queryName = "user_list";
String query = ApplicationController.getQuery(queryName);
PreparedStatement statement;
try {
statement = ApplicationController.getConnection().prepareStatement(query);
ResultSet resultSet = statement.executeQuery();
// Parcours des lignes retournées
while (resultSet.next()) {
UserBean bean = new UserBean();
bean.setId(resultSet.getInt(UserBean.FIELD_ID));
bean.setEmail(resultSet.getString(UserBean.FIELD_EMAIL));
bean.setLogin(resultSet.getString(UserBean.FIELD_LOGIN));
bean.setPassword(resultSet.getString(UserBean.FIELD_PASSWORD));
result.addBean(bean);
}
} catch (SQLException e) {
result = new WSResult("Erreur SQL.", WSResult.ERROR_CODE_SQL);
result.setErrorCode(e.getErrorCode());
result.setErrorMessage(e.getMessage());
}
return ok(Json.toJson(result));
}
}
Explications :
sql
, pour préparer les requêtes et les exécuter.UserBean
ici puisqu'il s'agit d'utilisateurs) et on l'ajoute à la liste de Beans de l'objet Result
.Result
transformé en JSON.Result
contenant le message et le code d'erreur SQL.Dans la méthode ci-dessus, on ne voit aucun code SQL.
Les requêtes sont stockées dans un fichier qui les regroupe toutes : conf/sql.properties
.
Comme tout fichier .properties
standard, chaque ligne est de la forme clé = valeur :
user_list = SELECT * FROM user
admin_user = SELECT * FROM user WHERE login = 'admin'
Ces requêtes sont récupérées dans les méthodes java à partir de leurs clés :
String queryName = "user_list";
String query = ApplicationController.getQuery(queryName);
Finalement, si vous tapez http://localhost:9000/userList
dans votre navigateur,
vous obtenez quelque chose de la forme :
En résumé :
route
la méthode correspondante et l'appelle.Result
.Result
est transformé en JSON et Play! vous en retourne le flux.Remarque : Si vous avez un affichage du JSON illisible, il existe une extension Firefox très utile.
Modifié le :
Publié le :
Pour créer un dump de votre base de données, utilisez la commande suivante :
mysqldump --host=localhost --port=3306 --databases database1_name database2_name --user=my_user --password=my_password > path/to/dump.sql
Par défaut :
localhost
3306
Vous devez spécifier la ou les bases de données à sauvegarder, ainsi que les login et mot de passe d'un utilisateur ayant le droit de consultation de la base.
La commande d'import est similaire à celle de dump :
mysql --host=localhost --port=3606 --user=my_user --password=my_password --default_character_set utf8 database_name < path/to/dump.