Astuce Afficher le numéro de version

Pour simplifier les mises à jour de votre application, il est préférable de n'indiquer le numéro de version qu'à un seul endroit. Cet endroit peut être un fichier mis à jour manuellement ou via la CI. Par exemple le package.json.

Ex :

{
  "name": "my-app",
  "version": "0.0.1",
  [...]
}

Pour récupérer le numéro de ce fichier, vous pouvez utiliser simplement les imports javascript :

// src/app/App.tsx
import { version } from '../../package.json';

Si vous utilisez typescript, cela nécessite d'activer l'option resolveJsonModule dans tsconfig.json :

{
  "compilerOptions": {
    [...]
    "resolveJsonModule": true,
    [...]
  }
}

Astuce Modifier les métadonnées

Dans une application React, vous pouvez définir des métadonnées via des balises <meta> à l'intérieur de la balise <head>, directement dans le fichier index.html.

<!doctype html>
<html lang="fr">
<head>
    <meta charset="UTF-8"/>
    <base href="/"/>
    <meta name="viewport" content="width=device-width, initial-scale=1.0, interactive-widget=overlays-content">
    <meta name="description" content="A sample app that introduces useMetaTag and useTitleTag hooks"/>
    <title>My Awesome app v1.1</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

Malheureusement, ces données sont statiques et ne s'adaptent pas dynamiquement à la page en cours. Pour palier cela, la solution consiste à modifier ou ajouter ces balises dynamiquement dans le DOM en javascript.

Le projet React Helmet devrait convenir à cette tâche, mais ne semble plus maintenu.

Voici deux hooks simples permettant de gérer vous-même vos balises <meta> et <title>.

Balises <meta> :

/**
 * Crée ou met à jour dynamiquement une balise meta dans le head de la page.
 *
 * @param {string} name Valeur de l'attribut `name` de la balise
 * @param {string} content Valeur de l'attribut `content` de la balise
 */
export const useMetaTag = (name: string, content: string) => {
    let metaElement = document.querySelector(`meta[name='${name}']`);
    if (!metaElement) {
      metaElement = document.createElement('meta');
      metaElement.setAttribute('name', name);
      document.head.appendChild(metaElement);
    }

    metaElement.setAttribute('content', content);
  };

Balise <title> :

/**
 * Crée ou met à jour dynamiquement une balise title dans le head de la page.
 *
 * @param {string} title Titre de la page
 */
export const useTitleTag = (title: string) => {
    let titleElement = document.querySelector('title');
    if (!titleElement) {
      titleElement = document.createElement('title') as HTMLTitleElement;
      document.head.appendChild(titleElement);
    }

    titleElement.textContent = title;
  };

Exemple d'utilisation :

import { useNavigation } from './helpers/useNavigation.ts';
import { useTitleTag } from './helpers/useTitleTag.ts';
import { version } from '../../package.json';

export const App = () => {
  useMetaTag('description', 'A sample app that introduces useMetaTag and useTitleTag hooks');
  useTitleTag(`My Awesome app - v${version}`);
}

Note : Si vous souhaitez des métadonnées spécifiques à une certaine page, utiliser ces hooks dans votre composant associé à votre route plutôt que dans le App.tsx.

Astuce Structure d'un projet React

Comme pour pas mal d'aspects, React ne propose pas vraiment de solution standard pour organiser la structure de son projet.

Voici quelques pistes possibles, se rapprochant de ce qu'on peut trouver dans l'écosystème React.

Structure simple

src/
- components/
  - MyComponent/
    - MyComponent.tsx
- helpers/
- hooks/
- model/
  - MyEntity.ts
- services/

On regroupe ici chaque élément par type (les hooks avec les hooks, les composants avec les composants, les interfaces du modèle entre elles…). Au fur et à mesure que le projet grossit, on ajoute des sous répertoires. Par exemple :

src/
- components/
  - forms/
    - fields/
      - MyField/
        - MyField.tsx
    - MyForm/
        - MyForm.tsx
  - pages/
    - MyPage.tsx
- helpers/
- hooks/
- model/
  - interfaces/
    - MyEntity.ts
  - enums/
    - MyEnum.ts
- services/

Atomic design

Cette approche se focalise sur le découpage des composants, et nous pousse à réfléchir à leurs responsabilités. Pour le reste, on peut conserver la structure précédente.

src/
- components/
  - atoms/
    - MyField/
      - MyField.tsx
  - molecules/
    - MyForm/
      - MyForm.tsx
  - organisms/
    - MyForm/
      - MyForm.tsx

Le principe, c'est de hiérarchiser les composants en 3 groupes :

  • les atomes, sont les composants élémentaires de plus petit niveau, à la base de tout. On y trouve typiquement des boutons, des champs de formulaires… Bref, des composants réutilisables et sans aucun rapport avec le métier.
  • les molécules constituent le niveau intermédiaire, et représentent des ensembles fonctionnels réutilisables. On peut y ranger les formulaires, les listes…
  • les organismes correspondent au niveau le plus haut, typiquement à une page simple. Par exemple une liste de tâches, avec des filtres et une pagination.

Les dépendances sont interdites vers un niveau supérieur. Autrement dit, vos atomes ne doivent dépendre de rien.

Vous en saurez plus avec cet article de blog : https://blog.logrocket.com/atomic-design-react-native.

Bonus :

Si vous devez gérer des pages complexes, qui proposent plusieurs fonctionnalités indépendantes, mais sur un même écran, vous pouvez ajouter un quatrième niveau pages/, qui servira de passe-plat entre le routeur et les organismes.

Cela permet aux organismes de s'affranchir des paramètres d'URL et ajoute plus de souplesse si tout à coup, vous devez remplacer votre page de création d'utilisateur par une fenêtre modale sur votre page de liste.

De plus, comme pour la structure simple, vous pouvez créer des sous-répertoires pour regrouper vos composants (ex : buttons/, fields/, lists/, cards/…).

Features

Le problème des deux types de structures précédentes, c'est que les fichiers sont disséminés un peu partout, plutôt que d'être regroupés par zone fonctionnelle. Si vous voulez supprimer une page, vous devrez peut-être supprimer des fichiers dans model/, components/ et hooks/.

Pour éviter cela, vous pouvez utiliser la structure proposée sur le dépôt bulletproof-react.

Le principe est de créer un répertoire features/, contenant autant de sous-répertoires que de zones fonctionnelles de votre application. Dans chacun d'eux, on y retrouvera la structure simple (voire Atomic design) présentée précédemment.

Par exemple :

plusieurs features

Cette structure n'a d'intérêt que si l'on respecte une hiérarchie entre les features. Une première feature peut dépendre d'une ou plusieurs autres, mais ces deux autres ne doivent pas dépendre de la première. Pour résumer, attention aux dépendances cycliques.

L'exercice n'est pas forcément simple, et si l'on n'arrive pas à éviter les dépendances bidirectionnelles entre deux composantes métier, c'est qu'elles sont trop intriquées pour les séparer en deux features distinctes.
On peut alors éventuellement utiliser une troisième feature (ex : core), dont dépendront les deux autres, ou placer les fichiers transversaux dans des répertoires à la racine de src/ comme proposé par bulletproof-react.

Astuce Gestionnaire de rollbacks

Problématique

La plupart des SGBD proposent un fonctionnement en transactions qui permet, en cas d’erreur, de retrouver les données telles qu’ells étaient au début de la transaction.

Schématiquement, cela se passe ainsi :

  • On déclare le début d’une transaction
  • On effectue toutes les opérations que l’on souhaite
  • Si l’on considère que tout s’est bien passé, on valide la transaction et toutes les modifications de données induites par les opérations sont entérinées
  • Si, à l’inverse, une opération échoue, on effectue un rollback qui remet les données comme elles étaient au début

Ce fonctionnement est facile à mettre en œuvre dans Symfony, avec Doctrine. C’est plutôt simple et efficace, mais cela ne concerne que les écritures en base de données.

On peut avoir exactement le même besoin pour des écritures via des appels API.

Exemple avec une création de compte

  • Je crée un compte utilisateur dans mon application (donnant lieu à une écriture en base de données)
  • J’appelle une API externe d’authentification, pour lui demander de créer ce compte utilisateur (dans sa base de données)
  • J’appelle une autre API externe de gestion de fiches client, pour lui demander d’en créer une nouvelle pour cet utilisateur (dans sa base de données)

L’appel à la seconde API échoue. Sans fiche client, l’application ne fonctionnera pas correctement et on préfère donc annuler complètement la création du compte. Il nous faut donc annuler :

  • l’écriture dans la BDD de l’application
  • l’écriture réalisée auprès de la 1re API

Cet exemple est encore relativement simple s’il n’y a que deux traitements à annuler, mais s’ils sont nombreux, un service dédié aux rollbacks va vite devenir utile.

Service de gestion de rollbacks

Voici un exemple simple d’un tel service, avec deux méthodes

  • registerRollbackOperation() : ajoute une opération de rollback dans un registre interne au service
  • rollbackAll() : exécute toutes les opérations de rollbacks du registre

Note :

Contrairement à celui de Doctrine, on ne déclare pas de début de transaction, et on ne valide pas de transaction.
On ne peut donc pas réinitialiser le registre pour commencer une seconde transaction.
On peut tout de même gérer plusieurs lots de rollbacks indépendants, en utilisant plusieurs instances du service en parallèle.

<?php

namespace App\Service;

use Psr\Log\LoggerInterface;
use Symfony\Component\Serializer\SerializerInterface;

class RollbackService
{
    private array $rollbackOperations = [];

    public function __construct(
        private readonly SerializerInterface $serializer,
        private readonly LoggerInterface $logger,
    ) {
    }

    /**
     * Ajoute une opération de rollback au registre.
     *
     * @param string $pool Lot d’opération
     * @param callable $rollbackFunction Fonction à exécuter lors du rollback
     * @param array $rollbackArguments Arguments à passer à la fonction de rollback
     * @param array $context Contexte de l’opération, utile pour les logs
     *
     * @return void
     */
    public function registerRollbackOperation(
        string $pool,
        callable $rollbackFunction,
        array $rollbackArguments,
        array $context,
    ): void {
        $this->rollbackOperations[] = [
            'pool' => $pool,
            'function' => $rollbackFunction,
            'arguments' => $rollbackArguments,
            'context' => $context,
        ];
    }

    /**
     * Dépile et exécute les opérations de rollback du registre, une à une, de la dernière à la première ajoutée.
     *
     * @return void
     */
    public function rollbackAll(): void
    {
        $totalNbOfOperations = \count($this->rollbackOperations);
        $index = 0;
        while ($rollbackOperation = array_pop($this->rollbackOperations)) {
            $index++;
            $this->logger->debug(
                sprintf('Rollback %s/%s - %s', $index, $totalNbOfOperations, $rollbackOperation['pool']),
            );

            try {
                $rollbackOperation['function'](...$rollbackOperation['arguments']);
                $this->logger->debug(
                    sprintf(
                        'Rollback %s/%s successful - %s %s',
                        $index,
                        $totalNbOfOperations,
                        $rollbackOperation['pool'],
                        $this->argsAndContextToString($rollbackOperation),
                    ),
                );
            } catch (\Exception $e) {
                $this->logger->error(
                    sprintf(
                        'Failure for rollback %s/%s - %s : %s %s',
                        $index,
                        $totalNbOfOperations,
                        $rollbackOperation['pool'],
                        $e->getMessage(),
                        $this->argsAndContextToString($rollbackOperation),
                    ),
                    ['exception' => $e],
                );
            }
        }
    }

    private function argsAndContextToString(array $rollbackOperation): string
    {
        return sprintf(
            '(args : %s; context : %s)',
            $this->encode($rollbackOperation['arguments']),
            $this->encode($rollbackOperation['context']),
        );
    }

    private function encode(mixed $context): string
    {
        return $this->serializer->serialize($context, 'json');
    }
}

Exemple d’utilisation, combiné avec les transactions Doctrine:

<?php

namespace App\Service;

use Doctrine\ORM\EntityManagerInterface;
use App\Entity\User;

class AccountCreationService
{
    public function __construct(
        private readonly EntityManagerInterface $entityManager,
        private readonly Api1Service $api1Service,
        private readonly Api2Service $api2Service,
        private readonly RollbackService $rollbackService,
    ) {
    }

    public function createAccountFromAdminInterface(array $data): void
    {
        $this->entityManager->beginTransaction();
        try {
            $this->persistUser($data);
            $this->createAccount($data);
            $this->createCustomerSheet($data);

            $this->entityManager->commit();
        } catch (\Exception $e) {
            $this->rollbackService->rollbackAll();
            $this->entityManager->rollBack();

            throw $e;
        }
    }

    public function persistUser(array $data): void
    {
        $this->entityManager->persist(User::fromData($data));
        $this->entityManager->flush();
    }

    public function createAccount(array $data): void
    {
        $accountId = $this->api1Service->createAccount($data);

        $this->rollbackService->registerRollbackOperation(
            'API 1',
            $this->api1Service->deleteAccount(...),
            [$accountId],
            [
                'description' => 'Account creation from admin interface',
            ],
        );
    }

    public function createCustomerSheet(array $data): void
    {
        $customerSheetId = $this->api2Service->createCustomerSheet($data);

        $this->rollbackService->registerRollbackOperation(
            'API 2',
            $this->api2Service->deleteCustomerSheet(...),
            [$customerSheetId],
            [
                'description' => 'Account creation from admin interface',
            ],
        );
    }
}

Explications :

  • À la création d’un compte depuis l’interface d’admin, on veut exécuter 3 opérations, que l’on place dans un try-catch
  • Comme on veut être capable de réinitialiser des données dans la BDD de notre application via Doctrine, on encadre ces opérations avec les méthodes beginTransaction() et commit() de l’entity Manager.
  • Les deux méthodes de création auprès des API sont similaires : on demande la création d’un utilisateur, puis on déclare sa suppression comme opération de rollback.
  • En cas d’erreur, on déclenche les opérations de rollback de notre RollbackService et le rollback de Doctrine.

Notes :

  • La méthode User::fromData() simule l’instanciation d’une entité User à partir d’un tableau de données
  • La notation myfunction(...) permet de générer une callable depuis PHP 8.2.
  • On utilise le Sérialiseur de Symfony pour transformer les tableaux de contexte et d’arguments en chaîne JSON pour les logs

Aller plus loin

Organisation des services

Si on appelle à plusieurs endroits les fonctions de création pour les API, on peut imaginer la structure suivante :

  • Service\Api1Service
  • Service\Api1ServiceWithRollbacks

Le premier service effectue les appels basiques à l’API (typiquement des requêtes HTTP).
Le second service utilise le premier. Il contient des méthodes comme le createCustomerSheet() ci-dessus, qui gèrent plusieurs appels successifs à l’API.

Rollback des suppressions/modifications

Pour rétablir les données telles qu’avant suppression ou modification, il faut les passer en argument de l’opération de rollback. Pour cela, il faut déjà les avoir sous la main, et donc les avoir récupérées auprès de l’API.

On peut donc imaginer une méthode updateCustomerSheet() qui effectue ces étapes :

  • Appel de l’API pour mémoriser la fiche client actuelle
  • Appel de l’API pour mettre à jour la fiche avec les nouvelles données
  • Déclaration de l’opération de callback : un appel à l’API pour mettre à jour la fiche avec les données mémorisées

RollbackService

On pourrait implémenter un RollbackService plus complet/complexe, permettant :

  • de gérer des lots d’opérations de rollback, que l’on pourrait appeler transactions
  • d’intégrer directement le rollback doctrine

Astuce Qui utilise le port ?

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

Marque-page Marque-pages React

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.

Socle

Mise en place

On peut initialiser facilement une application vierge React + Vite avec typescript, via la commande npm create vite@latest my-app -- --template react-ts.

Routage

Le module react-router fait ça très bien.

Appels API

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.

État

Si on souhaite gérer un état globale de l’application au niveau local, on peut utiliser redux avec redux-toolkit ou alors recoil.

Formulaires

Le module react-hook-form fait ça très bien. Il supporte notamment Yup pour gérer la validation de champs.

Internationalisation

Le module react-i18next permet de gérer une appli multilingue.

Design

CSS

Pour restreindre des classes CSS à un composant en particulier, on peut utiliser le concept de modules CSS.

Material

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.

Qualité

Tests

Plusieurs bibliothèques pour les tests fonctionnent bien avec React, notamment jest.

Lint

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.

Documentation des composants

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.

Autres

Base de donnée locale

Pour utiliser plus facilement la base de données locale IndexedDB, on peut y accéder via dexie.

Authentification avec JWT

Pour extraire les données d’un jeton JWT, on peut utiliser jwt-decode.

Marque-page Modèle de composant

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 :

  • Le composant est utilisable dans un autre composant, via <MyComponent children={<p>EXAMPLE</p>} />
  • On importe le fichier MyComponent.module.scss, dans lequel se trouve une règle CSS pour la classe myComponentClass.
  • Le composant est un wrapper, auquel on passe un sous-composant via la propriété children.
  • On déclare l’interface Props pour définir les propriétés du composant. Cela facilite l’autocomplétion et permet la validation du typage à la compilation.
  • La propriété notRequiredProperty est facultative lorsqu’on appelle le composant.

Héritage

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 :

  • Les propriétés du composant sont l’union de celles de SomeComponentToWrapProps, avec celle déclarée ici.
  • On indique les quelques propriétés dont on a explicitement besoin, et on peut accéder à toutes les autres via l’objet others.

Astuce Debounce en React

Régulièrement en Javascript, on veut écouter les changements sur un élément, mais ne les prendre en compte qu’à intervalles réguliers.
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.

Script

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;
};

Exemple d’utilisation

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;

Marque-page Construire une queryString

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 :

  • Chaque propriété de l’objet devient une clé de la queryString, dont la valeur échappée.
  • Les paramètres sont séparés par & et ? est ajouté en préfixe de la chaîne.

Astuce Modifier l'auteur des commits Git

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.

Astuce Analyser l’occupation de l’espace disque

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.

Astuce SessionStorage vs LocalStorage

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.

cookie vs Storage

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.

session vs local

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.

Pour aller plus loin

Marque-page Refactoring avec Rector

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 :

  • Opérateurs ?? et ?:
  • Promotion de propriété de constructeur

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.

Erreur Impossible de charger les dépendances depuis https://jcenter.bintray.com

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.

Astuce Les formulaires dans Symfony

Cet article est une synthèse de la documentation officielle : Forms.

Généralités

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.)

Type de champ/formulaire

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.

Définition de formulaire

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;
    }
}

Extension de type

Pour des modifications légères de types déjà existants (par exemple ajouter des options), on peut implementer l'interface FormTypeExtensionInterface.

Documentation

Création d'un formulaire

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 :

  • du mapping Doctrine (pour les entités)
  • du typage PHP des propriétés (>= 7.4)
  • des contraintes de validation

Vérification de la requête

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 :

  • Vérification du verbe HTTP (POST, PUT, ...)
  • Récupération des données depuis la query ou dans le corps, selon la configuration
  • Vérification des paramètres serveur (ex: post_max_size)
  • Soumission éventuelle du formulaire

Soumission du formulaire

À la soumission du formulaire (ak. méthode submit()) :

  • les données sont enregistrées dans le formulaire
  • chaque champ est validé, selon ses contraintes
    • en cas d'erreur, la méthode addError() est appelée sur le champ
  • les évènements sont dispatchés

Listes des options d'un FormType

La 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

Rendu avec Twig

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.

Choix du thème

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' %}

Surcharge du thème

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.

Data transformers

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ères
  • reverseTransform() : (Tableau de) chaîne(s) de caractères => donnée

Pour 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());

Évènements

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.

Astuce Les contrôleurs dans Symfony

Cet article est une synthèse de la documentation officielle : Controller.

Généralités

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 :

  • passer le service contrôleur en public (car ce sera un point d'entrée de l'application, donc non appelé explicitement par nos services)
  • injecter les éléments demandés dans les signatures des actions (des services, des arguments, la Request), via un ArgumentResolver. (Cf. classe RegisterControllerArgumentLocatorsPass) Un contrôleur doit-il retourner absolument une Response ? Pas obligatoirement.
    Si ce n'est pas le cas, le HttpKernel dispatche un ViewEvent. Un listener pourra traiter cet évènement pour forwarder vers un contrôleur en fallback.

    AbstractController

    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).

    Service Locator

    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()).

    Redirections et Forwards

    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.

    Redirection

    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')
    );
}

HttpException

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.

Request

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 POST
  • cookies
  • files : fichiers téléversés
  • server : variables $_ENV et $_SERVER
  • headers : 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.

    Récupération des valeurs

    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()

    Cookies

    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.

    Session

    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).

    Flash message

    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.

Dans twig

Ce bag est accessible dans Twig via app.flashes :

{% for message in app.flashes('notice') %}
    <div class="flash-notice">
        {{ message }}
    </div>
{% endfor %}

Contrôleurs Symfony spécifiques

Symfony fournit deux contrôleurs spécifiques :

Astuce Créer une page

Cet article est une synthèse de la documentation officielle : Create your First Page in Symfony.

Généralités

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.

Routes et Controllers

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.

Annotations

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')]

Liste des routes

Pour lister les routes disponible et les contrôleurs associés, utilisez la commande suivante :

bin/console debug:router

Moteur de template

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.

Structure du projet

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 !)

Astuce Twig dans Symfony

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

Blocs

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 :

  • s'il est dans le même contexte que le template courant :
    • dans le même fichier
    • dans un template parent (cf. héritage ci-après)
    • dans un template "utilisé" (via le tag use), à la manière d'un Trait PHP
  • ou si on indique le fichier dans lequel il se trouve explicitement
{# 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.

Macros

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 :

  • qu'elle peut recevoir des arguments
  • elle n'est pas affichée/exécutée/rendue automatiquement (il faut l'appeler explicitement)

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() }}

Héritage

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.

Surcharge de blocs externes

À 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()) .

Variables globales

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).

Variables internes

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 courant

Variables globales Symfony

Symfony (ou plutôt son Twig Bridge) en ajoute une pour nous : app (documentation).

Ajouter des variables globales

On peut en ajouter

  • directement via un fichier de config :
# config/packages/twig.yaml

twig:
  globals:
    my_var: 'value'
    my_param1: '%some_parameter%'
    my_service: '@my_service'
    my_array_var:
      - 'Monday'
      - 'Tuesday'
  • dans une extension Twig, qui doit alors implémenter GlobalInterface et sa méthode getGlobals().

Inclusions

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).

Inclusion avec surcharge

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.

Filtres et fonctions

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')) }}

Opérateurs logiques

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().

Tests

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().

Extensions Twig

Documentation

Une extension Twig permet d'ajouter des filtres et fonctions personnalisés, mais également des tags, des opérateurs logiques, des tests, ...

Ajout de filtres et fonctions

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.

Dépendances dans une extension

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.

Échappement

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.

Échappement par défaut

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.

Type d'échappement

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.

Annuler l'échappement

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.

Rendu de contrôleur

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 :

  • La fonction render() appelle une URL et en affiche la réponse
  • La fonction controller() retourne la réponse du contrôleur en argument

Utiliser 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.

Http cache

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.

Traduction

Pour traduire une chaîne, on peut utiliser le filtre trans ou le bloc {% trans %}{% endtrans %}.
Le premier échappe la chaîne.

Traductions conditionnelles

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).

Documentation

Autres

Filtre de tableau

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 %}

sprintf()

Le filtre format() disponible dans les templates équivaut au sprintf() de PHP.

{{ 'My string with %d word(s).'|format(5) }}

dump()

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 %})

Déconnexion

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.

Formatage

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.

Astuce Le routage dans Symfony

Cet article est une synthèse de la documentation officielle : Routing.

Généralités

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 :

  • le ou les chemins concerné(s)
  • le contrôleur cible
  • le contexte pour lequel cela doit matcher (telle IP, telle méthode HTTP, ...)

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
{
    // ...
}

Regex

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 :

  • La première route utilise la liste des requirements pour définir la regex du paramètre page.
  • La seconde utilise la notation abrégée indiquant la regex directement au niveau du paramètre.

Valeurs par défaut

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 :

  • Pour laseconde notation, s'il n'y a pas de regex, on indique la valeur par défaut juste après le paramètre (ie. "/blog/{slug?homepage}").
  • Si on ne mets rien après le ?, alors le paramètre devient optionnel

Générer des URL

La 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)

Redirection systématique

Le RedirectController fourni par Symfony, permet des redirections temporaires ou permanente

  • d'un chemin donné vers un autre
  • d'un chemin vers une route donnée

Il s'utilise directement par configuration dans le routing.yaml.


Paramètres internes de routage

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.


Conditions spécifiques

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
    {
        // ...
    }
}

Commande Symfony pour le routing

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ôleurs
  • bin/console debug:router name_of_the_route : pour afficher la configuration complète d'une route
  • bin/console router:match /some/url [--method=GET] : pour afficher si l'URL match avec une route et si oui laquelle

Route Loader

Documentation

Pour 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.

Astuce Architecture Symfony

Licences

  • Symfony est sous Licence MIT Licence
  • La documentation officielle est sous Licence Creative Commons (Attribution-Share Alike 3.0 Unported Licence)
  • Twig est sous licence BSD-3

Composants, Bundles et Bridges

  • 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


Best practices

La liste des best practices est disponible ici.


Arborescence

Recommandée

├── assets/
├── bin/
│   └── console
├── config/
├── public/
│   └── index.php
├── src/
│   └── Kernel.php
├── templates/
├── tests/
├── translations/
├── var/
│   ├── cache/
│   └── log/
├── vendor/

Personnalisation

Non recommandé :

  • config/ : peut poser problème pour le déploiement des recipes Flex

Configurable :

  • /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.


Flex

Documentation

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.

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) :

  • créer un fichier à tel endroit
  • ajouter telles lignes de configuration dans tel fichier
  • ...

Certains recipes sont officielles (= maintenues par la code team Symfony), d'autres contrib.

Elles sont toutes visibles sur https://flex.symfony.com.

Configurators

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 :

  • activer un bundle
  • copier un répertoire de configuration
  • ajouter des variables d'environnement

Les configurators natifs sont visibles ici.

Commandes composer pour Flex

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 recipe

Gestion des erreurs et exceptions

Lorsqu'une erreur (PHP) est levée :

  • L'ErrorHandler la transforme en exception, qui est levée

Lorsqu'une exception est levée :

  • L'HttpKernel l'attrape et dispatche un évènement kernel.exception
  • L'ErrorListener effectue un forward vers l'ErrorController
  • L'ErrorController retourne une Response contenant l'erreur formatée via le ErrorRenderer

Gestion des évènements

Documentation

La classe Event contient deux méthodes :

  • stopPropagation() : elle indique qu'on ne souhaite pas que les prochains listeners ne traitent l'évènement
  • isPropagationStopped() : 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).

Commande Symfony

Pour lister les évènements disponibles et leurs listeners, utiliser la commande :

bin/console debug:event-dispatcher

Listener vs Subscriber

  • Un Listener est un callable qui reçoit un Event.
  • Un Subscriber est une classe qui mappe des évènements à des Listeners

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.

Principaux évènements

Les évènements du Kernel sont visibles ici.


Versions de Symfony

Documentation

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

Backward compatibility promise

D'une version mineure à l'autre, Symfony garantie un fonctionnement à l'identique pour toute l'API publique (= ni @internal, ni @experimental).

Dépréciations

Documentation

Les dépréciations indiquent qu'une fonction/classe/... ne devrait plus être utilisée grâce à l'annotation @deprecated.

  • Elle doit préciser depuis quand la fonction/classe/... est dépréciée.
  • Elle peut indiquer la version à laquelle elle a été ajoutée, et la méthode à utiliser à la place.

Surcharger des éléments de Symfony ou de bundle

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 :

  • supprimer les imports des routes de ce bundle
  • déclarer sa propre route

Comment surcharger un contrôleur ?

On peut :

  • en surcharger la route (cf. ci-avant), pour pointer vers un nouveau contrôleur.
  • le décorer comme n'importe quel service

Comment modifier un service ?

On peut :

  • le décorer (best practice)
  • redéfinir sa définition (via le service.yaml) et le faire pointer vers un nouveau service
  • le modifier via le Compiler Pass

Interopérabilité

Le 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

Astuce Tips - Symfony

Console

Liste des commandes disponibles

bin/console

Conteneur de services

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

Configuration

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

Liste des routes et de leur contrôleur

bin/console debug:router --show-controllers

Affiche la liste de toutes les routes et les contrôleurs/actions associés :

Création auto

La commande make permet de générer un squelette de classe (Command, TwigExtension, ...) :

bin/console make

Dump

dump($someVariable);

Pour afficher une variable et stopper l'exécution :

// "Dump and Die"
dd($someVariable);

Twig

Pour afficher toute la syntaxe ainsi que les variables globales disponibles dans Twig :

bin/console debug:twig

Injection de dépendances

Argument binding via services.yaml

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) {}

Alias de service

Un alias de service peut être créé en une ligne :

services:
      Some\Path\To\SomeService $newName: '@id_of_the_target_service'

Logging

Monolog peut loguer des messages sur des channel spécifiques. Pour cela il faut :

  • Déclarer ce nouveau channel
# ex: dans monolog.yaml
monolog:
  channels: ['my_new_channel']
  • Utiliser le nouveau service référencé par Monolog (cf.bin/console debug:autowiring log)
public function __constructor(LoggerInterface $myNewChannelLogger) {}

Bundles sympas

StofDoctrineExtensionsBundle

Ajoute des extensions doctrine (Slug, Blameable, Softdeleteable, ...)

Dépôt Git

KnpTimeBundle

Ajoute le filter twig ago, pour afficher depuis quand la date est passée.

Dépôt Git

Foundry

Ajoute une commande Symfony (make:factory) pour générer des Factory pour les entités.

Dépôt Git

Compatible avec le bundle ci-dessous

FakerBundle

Permet de générer du faux contenu de manière intelligente.

Dépôt Git

Astuce Installation de Symfony

Cet article est une synthèse de la documentation officielle : Installing & Setting up the Symfony Framework.

Prérequis

  • Symfony 4.4 : PHP 7.1 / Symfony 5.3 : PHP 7.2
  • Extensions PHP : Ctype, iconv, JSON, PCRE, Session, SimpleXML et Tokenizer
  • Composer

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.

Installation

Nouveau projet

L'installation consiste à initialiser un nouveau projet Symfony.

Deux choix principaux s'offrent à vous :

  • installer la version minimale
  • installer une version plus complète, avec la majorité des composants nécessaires à une application web
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.

Récupération d'un projet existant

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.

Permissions sur les fichiers

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).

Démarrage de l'application

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

Ajout de dépendances

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

Sécurité des dépendances

Pour vérifier la présence de failles connues parmi les dépendances de votre projet, symfony propose une commande :

symfony check:security

Marque-page C4 - Modèle de visualisation d'architecture logicielle

C4 est une proposition d'organisation graphique pour représenter une architecture logicielle sous forme de schémas.

Documentation : https://c4model.com

Échelles

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.

4 niveaux

Context

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 :

  • les utilisateurs
  • les autres systèmes interagissant avec elle

Containers

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 :

  • une webapp Angular
  • une API Symfony
  • Une BDD PostgreSQL

Components

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 :

  • l'inscription
  • la gestion des utilisateurs
  • l'affichage des actualités
  • la newsletter

Cela correspond souvent aux premiers niveaux de namespace/package/répertoires/etc.

Code

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.

Conclusion

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.

Représentation

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é.

Outils

Pour faciliter la création de schémas, le site officiel recense plusieurs outils, dont :

  • PlantUML : un langage permettant de générer des schémas à partir de fichiers textuels
  • diagrams.net : une interface graphique gratuite en ligne

Erreur Google Map vide

Si vous intégrez une Google Map mais qu'elle reste vide, vérifiez bien :

  • que les scripts javascript Google sont bien chargés
  • que l'initialisation de la google Map est correcte, sans oublier les paramètres center et zoom.

Voici l'exemple fourni par Google dans la documentation.

Astuce Créer un patch avec TortoiseSVN

TortoiseSVN permet de générer des patchs pour passer d'une version à une autre du projet.

Il y a deux types de patch :

  • Les patchs contenant toutes les lignes des fichiers modifiés qui ne sont pas encore "commités".
  • Les patchs contenant l'arborescence de tous les fichiers modifiés entre deux tags.

Description

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.

Utilisation

Pour générer un fichier .patch, il suffit de :

  • Faire un clic-droit sur l'arborescence
  • > TortoiseSVN
  • > Create patch...
  • Sélectionner les fichiers à patcher
  • Choisir (via le bouton Options) si on considère les modifications d'espaces blancs.

Pour utiliser ce fichier, il faut :

  • Le placer à l'endroit d'où on l'a généré
  • Faire un clic-droit sur le fichier
  • > TortoiseSVN
  • > Apply patch...

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.

Pour générer toute l'arborescence des fichiers modifiés entre deux tags, il faut :

  • Faire un clic-droit sur l'arborescence
  • > TortoiseSVN
  • > Repo-browser
  • Faire un clic-droit dans l'arborescence, sur la version la plus récente à comparer
  • > Mark for comparison
  • Faire un clic-droit dans l'arborescence, sur la version la plus ancienne à comparer
  • > Compare URLs

La liste des fichiers modifiés entre les deux versions apparaît. Il suffit alors de :

  • Les sélectionner tous
  • Faire un clic-droit sur un fichier sélectionné
  • > Export selection to...
  • Choisir où générer les fichiers

Astuce Supprimer le menu contextuel créé lors de l'installation de Git

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

Erreur Docker écrase le réseau VPN

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.

Astuce Liste des caractères unicode d'espaces spéciaux

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}',
  ];

Astuce Autoriser Composer à utiliser toute la mémoire dont il a besoin

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 :

  • On lance Composer via PHP, ce qui permet de surcharger la valeur de memory_limit (-1 pour infinie)
  • which est une commande système qui affiche le chemin complet vers un exécutable (ici composer)
  • On ajoute ensuite les arguments classiques à passer à Composer

Astuce Bypasser un runlevel

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).

Astuce Éviter les injections de wildcards dans les requêtes avec Doctrine

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, '%_'));

Astuce Les étapes d'authentification via Ldap dans Symfony

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 :

  • Authentification au Ldap
  • Recherche de l'utilisateur correspondant au login
  • Vérification du couple login/mot de passe

Selon l'étape, différentes classes et paramètres du framework seront utilisés.

Authentification au Ldap et recherche de l'utilisateur

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

Authentification

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.
  • Le searchDn est un "chemin" pour Ldap, permettant de trouver un utilisateur. Il est composé de son CN (Common Name) et de la baseDn.

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)%.

Recherche de l'utilisateur

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).

Vérification du couple login/mot de passe

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é.

Astuce Utiliser un schéma spécifique pour la base de données PostgreSQL

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;

Astuce Supprimer les images/conteneurs/volumes docker

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

Astuce S'authentifier dans le Swagger d'API Platform

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 :

Bouton d'authentification 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

Erreur [Symfony 4] L'utilisateur connecté est anonyme

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 :

  • Vous vous connectez via la mire de login en front, recevez bien un jeton JWT en réponse
  • Vous lancer une autre requête auprès du backend, et vous voyez bien (dans la console de votre navigateur) que le jeton JWT est passé dans les headers
  • Dans votre contrôleur Symfony, vous récupérez l'utilisateur connecté (par exemple via 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).

Astuce Auditer du code PHP

Voici trois outils utiles pour auditer du code PHP de manière statique :

PHPStan

Installation

cd my_project
wget https://github.com/phpstan/phpstan-shim/raw/master/phpstan.phar

Utilisation

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'option -l permet de choisir le niveau d'acceptation des erreurs, avec 0 le moins strict possible, et 7 pour le plus strict.

SonarQube

Installation

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 :

  • Pour retirer certains fichiers/répertoires de l'anlayse, utilisez l'option sonar.exclusions
  • Les autres options sont listées dans la documentation officielle

Lancez SonarQube via Docker :

docker pull sonarqube
docker run -d --name sonarqubedocker_sonarqube_1 -p 9000:9000 sonarqube
  • Accédez à SonarQube avec votre navigateur via cette adresse : http://localhost:9000.
  • L'interface indique que le site est en cours de maintenance. Attendez qu’il soit opérationnel.
  • Connectez-vous en tant qu'administrateur (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

Utilisation

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é.

Accueil rapport SonarQube

PHPStorm

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...

Rapport PHPStorm

Le rapport peut ensuite être exporté au format HTML ou XML.

Astuce Mocker des fonctions natives PHP

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);
    }
}

Gérer le retour d'une fonction native

À 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 {}.

Gérer plusieurs retours d'une fonction native

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.

DateTime

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
        }
    }

    // [...]
}

Astuce Savoir quel processus utilise un port

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).

Astuce Les nouveaux opérateurs introduits par PHP 7

PHP 7 introduit 2 nouveaux opérateurs : ?? et <=>, nommés respectivement Null coalescent et Spaceship.

Null coalescent

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.

Spaceship

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.

Astuce La fonction __invoke()

Utilisation

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.

Utilité

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

Marque-page Introduction à NodeJS

Exercices

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.

Accueil 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

Express

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)

Mongoose

Pour utiliser une base MongoDB avec NodeJS, il existe l'API Mongoose.
(Elle est utilisée dans le tutoriel précédent.)

Astuce Utiliser les promesses

Introduction

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.

Exemples

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.'.

Aller plus loin

Les promesses existent depuis un moment en javascript. Il y a pas mal de cas d'utilisation :

  • exécuter trois tâches asynchrones une à une
  • exécuter trois tâches asynchrones une à une et en transmettant à chaque fois le résultat à la suivante
  • exécuter plusieurs tâches asynchrones en parallèle, puis une dernière quand elles sont toutes terminées
  • exécuter 2 séries de 3 tâches asynchrones en parallèle
  • ...

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 :

Astuce Centrer une popin Boostrap 3

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 :

Popin Bootstrap 3 non centrée verticalement

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 :

Popin Bootstrap 3 centrée verticalement

Astuce [eZ5] Authentifier un utilisateur programmatiquement

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);
    }

Marque-page Ouvrir un PSD sans Photoshop

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/.

Astuce Virtual host avec contexte d'URL

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.

Erreur [D8] Erreur lors de l'upgrade de Drupal à propos du module Views

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&#039;t
exist. dans Drupal\Core\Config\Schema\ArrayElement-&gt;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.

Astuce Installer un patch via composer

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 :

  • Le projet requiert la lib composer-patches comme dépendance.
  • On a ausssi intégré drupal/core, que l'ont veut patcher.
  • Après avoir téléchargé et installé cette dépendance, Composer va appliquer le patch présent à l'URL https://www.drupal.org/files/issues/2551373-35.patch.

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"
      }
    }
  }
}

Astuce [d8] Ajouter des variables dans un render array existant

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').
  • C'est cette clé qu'on utilise dans le nom de notre fonction hook_preprocess_HOOK().

Remarque :

Comme après chaque implémentation de hook(), pensez à vider les caches.

Marque-page Étendre la taille d'un disque virtuel

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.

Configuration des disques

Modifications côté Virtual box

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.

Prérequis

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).

Ajout d'espace disque

Il faut ensuite modifier la taille du disque, via la commande :

VBoxManage modifyhd "cloned.vdi" --resize 51200

51200 est la nouvelle taille en Mo.

Retour au format initial

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

Modification côté VM

Votre disque a maintenant la taille souhaitée, mais la partition n'a pas changé. Vous avez donc tout un espace disque non alloué.

  • Téléchargez un iso de gparted
  • Insérez-le dans le lecteur virtuel de votre VM
  • Bootez dessus
  • Suivez ce tutoriel
  • Redémarrez votre VM

Vous devriez maintenant avoir un disque dur plus grand et fonctionnel !

Astuce Installer PHP 7 sous Debian 8

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.

  • Ajoutez un nouveau dépôt pour 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
  • Installez PHP 7, avec les extensions que vous souhaitez. Probablement au moins 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.

Astuce Installation de PHP

Installation

PHP et ses extensions

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.

  • Installez PHP via le gestionnaire de paquets :
sudo apt-get install libapache2-mod-php5
  • Installez les extensions dont vous avez besoin. Probablement au moins 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.

Configuration de PHP

  • Modifiez le fichier /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"
  • Redémarrez Apache :
sudo service apache2 restart

Vérification

  • Supprimez le fichier index.html de votre site (ex : /var/www/mon-site/index.html) et créez le fichier index.php à la place :
<?php
phpinfo();
?>
  • Appelez l'URL de votre site et vérifier qu'Apache vous retourne bien toute la configuration de PHP.

Composer

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

Astuce Installation de PostgreSQL

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

Installation

  • Exécutez simplement la commande suivante :
sudo apt-get install postgresql postgresql-client postgresql-doc
  • Vérifiez votre version de postgres (ex: 9.4)
    ls /etc/postgresql/
  • Forcez postgreSQL à utiliser l'UTF-8. (Adaptez les commandes suivantes avec la bonne version) :
sudo  pg_dropcluster --stop 9.4 main
sudo  pg_createcluster --start -e UTF-8 9.4 main

Configuration générale

Connexion distante

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

Adminer

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 :

  • Vous pouvez bien sur adapter ces configurations, en particulier le nom de domaine db.adminer.dev.
  • Vous devrez probablement ajouter ce nom de domaine dans le fichier 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

Configuration pour votre site

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éé.

  • Connectez vous en tant qu'utilisateur postgres (qui est administrateur postgreSQL) :
sudo su - postgres
  • Créez un nouvel utilisateur :
createuser --interactive mypguser

Avec les options suivantes :

  • Super-utilisateur : Non
  • Créateur de base de données : Oui
  • Création de nouveaux rôles : Non

Modifiez son mot de passe :

psql
> ALTER USER mypguser WITH PASSWORD 'new_password';
  • Créez une nouvelle base de données :
createdb -O mypguser mypgdatabase
  • Si besoin, connectez-vous-y pour rendre votre utilisateur propriétaire du schéma public :
psql mypgdatabase
> ALTER SCHEMA public OWNER TO mypguser;
  • Redémarrez le service postgresql :
sudo /etc/init.d/postgresql reload

Utilisation

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).

Astuce Installer PHP 7 sous Debian 8

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.

  • Ajoutez un nouveau dépôt pour 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
  • Installez PHP 7, avec les extensions que vous souhaitez. Probablement au moins 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.

Astuce Installation d'Apache

Installation

Lancez simplement la commande suivante :

sudo apt-get install apache2

Configuration générale

Modification du charset

  • Éditez le fichier de configuration du charset :
sudo nano /etc/apache2/conf-available/charset.conf
  • Décommentez la ligne :
AddDefaultCharset UTF-8

Utilisateur Apache

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

Activation du module de réécriture d'URL

Pour activer le module rewrite d'Apache et redémarrez Apache, utilisez ces commandes :

sudo a2enmod rewrite
sudo service apache2 restart

Configuration propre à votre site

Initialisation de votre site

  • Créez le répertoire racine pour votre site :
cd /var/www
sudo mkdir mon-site
sudo chown -R phpuser:phpuser mon-site
  • Créez un fichier index.html à l'intérieur, contenant par exemple <h1>Hello world !</h1>

Configuration du virtual host

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

Vérification

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>

Astuce Installation d'un environnement LAPP sous Debian

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)

Le wizard d'installation de Debian

Dans cet exemple les configurations choisies sont en italique.

  • Choisissez 64 bit install au boot sur le CD.
  • Choisissez la langue et la localisation -> France
  • Indiquez le nom de la machine (= celui visible sur le réseau) -> debian-server
  • Indiquez le domaine réseau -> (laisser vide si vous n'êtes pas dans un réseau d'entreprise, sinon renseignez-le)
  • Indiquez et confirmer le mot de passe root -> **
  • Indiquez le nom de l'utilisateur courant à créer -> phpuser
  • Indiquez les identifiants (login/mot de passe) pour cet utilisateur -> phpuser/phpuser
  • Choisissez la méthode de partitionnement -> Assisté - utiliser un disque entier
  • Choisissez le disque à partitionner ->
  • Choisissez le schéma de partitionnement -> Tout dans une seule partition
  • Terminez le partitionnement et appliquez les changements
  • Appliquez les changements sur les disques
  • Configurez l'outil de gestion des paquets :
    • Langue -> France
    • Miroir -> ftp.fr.debian.org
    • Mandataire ->
  • Refusez l'envoi de statistiques à propos des paquets installés
  • Sélectionnez les ensembles de logiciels que vous voulez installer (au moins Serveur SSH) :
    • Environnement de bureau Debian (= mode graphique)
    • Serveur d'impression
    • Serveur SSH
    • Utilitaires usuels du système
  • Acceptez l'installation de GRUB
  • Continuez en démarrant le nouveau système

Modification du système

Installation de sudo

  • Connectez-vous en administrateur -> root/******
  • Installez la commande sudo :
apt-get install sudo

Configuration du sudo

Ajoutez l'utilisateur phpuser au groupe sudo :

adduser phpuser sudo

Autoriser la connexion SSH en root

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

Amélioration de la console

Modifier le fichier .bashrc permet d'améliorer l'affichage de la console et d'ajouter des alias de commande.

  • Connectez-vous avec l'utilisateur -> phpuser/phpuser
  • Modifiez le fichier .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\] '
  • Ajouter cette ligne à la fin du fichier, pour pouvoir exécuter les commandes de /usr/sbin facilement :
export PATH=$PATH:/usr/sbin
  • Enregistrez les modifications avec Ctrl+X

Mise à jour de la distribution et de la liste des paquets

  • Lancez les commandes suivantes pour procéder à la mise à jour :
sudo apt-get update
sudo apt-get upgrade
sudo apt-get dist-upgrade
  • Acceptez d'installer les éventuels paquets à mettre à jour
  • Modifiez le fichier /etc/apt/sources.list avec nano en tant qu'administrateur :
deb http://security.debian.org/ etch/updates main contrib non-free
  • Mettez à jour la liste des dépôts et installez les bibliothèques de base :
sudo apt-get update
sudo apt-get install build-essential

Autre

Installez les outils dont vous avez besoin (ex: Screen, ...).

Astuce Le module Help

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 : Message formulaire d'édition

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é) :

Configuration du module Help

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).

Astuce [D8] Ajouter un bouton d'action en back-office

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

  • la route (et ses paramètres) vers laquelle devra pointer le bouton d'action
  • le libellé du bouton (title)
  • sur quelle(s) page(s) il devra apparaître (= ids de routes)

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`.

Astuce Modifier un élément de liste dans un formulaire

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;
}

Astuce Rediriger en 404 ou 403

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();

Astuce Drupal dans un sous-répertoire

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

Astuce Connaître le poids et le nombre de fichiers d'un répertoire

Poids total

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).

Nombre de fichiers

Pour connaître le nombre de fichiers il faut cette fois composer avec plusieurs commandes :

find -L mon/repertoire -type f | wc -l

Explications :

  • La commande find avec l'option -type f permet de lister les fichiers
  • L'option -L permet de suivre les liens symboliques
  • La commande wc -l compte le nombre de lignes (ici celles retournées par find)

Erreur The following module is missing from the file system

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 :

  • Réinstaller le module, puis le désinstaller correctement
  • Supprimer la référence en base, via la requête suivante :
drush sql-query "DELETE FROM key_value WHERE collection='system.schema' AND name='module_name';"

Astuce Limiter la version à mettre à jour avec Composer

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 :

  • ~8.2.0 signifie >= 8.2.0 & < 8.3.0
  • ~8.2 signifie >= 8.2 & < 9
  • Il existe aussi le signe ^, moins restrictif : ^8.2.1 signifie >=8.2.1 & < 9

Astuce Retirer le versionnement d'un fichier avec IntelliJ/PhpStorm

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'

Astuce Rechercher un paquet

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 :

  • Si vous indiquez plusieurs termes, seuls les résultats avec chacun d'eux seront retournés
  • Les regexp sont gérées (au moins en partie). Ex : apt-cache search php.*memcach.

Si vous avez aptitude d'installé, une commande plus simple à mémoriser existe :

aptitude search terme1 terme2

Astuce Configurer le débogueur PHPStorm avec Apache sur une VM

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.

Installation de XDebug

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

Ajout d'un environnement distant dans PHPStorm

Ajoutez une nouvelle connexion SSH à votre VM :

  • Allez dans File > Settings... > Build, Execution, Deployment > Deployment
  • Cliquez sur le + vert à droite, pour ajouter un environnement
  • Choisissez un nom
  • Dans Connection, choisissez le type SFTP, et renseignez toutes les informations
  • Pour le Root path, indiquez le chemin vers le répertoire contenant vos sources sur votre VM (ex : /var/www/mon-site), ou un répertoire parent (ex : /var/www)
  • Dans Mappings, remplissez les 3 champs de chemin
    • Local path : la racine de votre projet sur la machine qui lance PHPStorm
    • Deployment path : le chemin relatif depuis le Root path défini précédemment, vers votre projet sur la VM (ex : / ou /mon-site)
    • Web path : / si vous utilisez un nom de domaine pointant directement vers votre projet
  • Validez

Précisez le mapping :

  • Allez dans File > Settings... > Languages & Frameworks > PHP > Server
  • Sélectionnez le serveur que vous venez de configurer
  • Pour chaque répertoire contenant des fichiers PHP à déboguer, cliquez à droite, dans la colonne Absolute path on the server
  • Saisissez le chemin absolu vers le répertoire correspondant sur votre VM (ex: /var/www/monsite/web ou /var/www/monsite/vendor)
  • Validez

Installation d'une extension pour navigateur

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.

Activation dans PHPStorm

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).

Début du débogage

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.

Erreur Problème de mémoire avec composer

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

Astuce Créer un lien symbolique

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.

Astuce [D8] Installer la dernière version de drush

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.

Prérequis

Composer et Git doivent être installés.

Composer

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

GIT

sudo apt-get install git

# Vérification
git --version

Installation

  • Téléchargez drush :
sudo git clone --depth 1 https://github.com/drush-ops/drush.git /usr/local/src/drush
  • Mettez-le à jour :
cd /usr/local/src/drush
sudo composer install
  • Créez les liens symboliques suivant :
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
  • Vérifiez l'installation :
drush --version

Astuce Suivre les liens symboliques

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

Astuce [d8] Surcharger l'affichage d'une page existante

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 :

  • Le 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.
  • Le 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.

Astuce Bonnes pratiques jQuery

Isoler son code

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.

Code à usage unique

Cela consiste à englober son code pour que les variables et fonctions deviennent locales.

(function($) {

    // Code à exécuter
    // [...]
})(jQuery);

Code à usage régulier

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);

Code affectant des éléments du DOM 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).

Code affectant des éléments du DOM 2

$.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).

Normes de codage et optimisations

Sélecteurs

// Pour des raisons de compatibilité, préférez
$('#container').find('.children').hide();

// à 
$('#container .children').hide();

// ou à 
$('.children', '#container').hide();

Éviter le tout jQuery

$('#link').on('click', function() {

    // Préférez
    this.id

    // à
    $this.attr(id');
});

Chargement du DOM

// Préférez 
$(function() {

});

// à 
$(document).ready(function() {

});

Gestion des évènements

// Préférez 
$('#element').on('click', function() {

});

// à 
$('#element').click(function() {

});

No conflict

// Préférez 
(function($) {
 // [...] Code sans conflit
})(jQuery);

// à 
jQuery.noConflict();
// [...] Code sans conflit

Appels AJAX

// Préférez 
$.ajax()

// à 
$.getJSON();

Astuce [D8] Ajouter des pages de configuration

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 :

Page d'administration en back-office

Pour générer deux pages de formulaires avec des onglets pour passer de l'un à l'autre, vous aurez besoin des fichiers suivants :

Arborescence nécesaire

Remarque :

L'exemple qui suit requiert d'activer ces deux modules : admin_toolbar et admin_toolbar_tools.

Création d'un formulaire d'administration

Voici un exemple de formulaire d'administration affichant trois champs, de types numérique, texte riche et email.

Structure générale

<?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 :

  • Le formulaire hérite de la classe abstraite ConfigFormBase, fournie par Drupal.
  • En plus des méthodes getFormId(), buildForm() et submitForm() habituelles, elle impose que la méthode getEditableConfigNames() soit implémentée.
  • Cette méthode permet de définir un "namespace" pour votre configuration. Chaque propriété devra avoir un id unique au sein du même "namespace".

Méthodes buildForm() et submitForm()

/**
  * {@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 :

  • Pour récupérer les valeurs présentes en base et préremplir les champs, on utilise la méthode config(), avec le "namespace" définit précédemment
  • Chaque valeur est accessible individuellement, via un simple getter
  • La méthode parente est appelée

La méthode submitForm() va enregistrer les données soumises en base :

  • Les configurations actuelles sont récupérées
  • Les nouvelles valeurs sont mises à jour, puis sauvegardées
  • La méthode parente est appelée

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.

Configuration du menu

my_module.routing.yml

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 :

  • Les deux dernières routes sont classiques. Elles pointent vers vos nouvelles pages de formulaire de configuration.
  • La première pointe vers un contrôleur fourni par Drupal, qui permet de lister des sous-pages (ex: http://www.monsite.com/admin/config/people)

my_module.links.menu.yml

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 :

  • le libellé (title)
  • la description au survol (attribut title pour le lien généré)
  • l'élément de menu parent
  • la route vers laquelle pointer
  • le poids de l'élément (le plus petit apparaîtra en premier)

Dans cet exemple, la page "overview" est parente des deux autres.

my_module.links.task.yml

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 :

  • le libellé (title)
  • la route vers laquelle pointer
  • l'onglet principal

Astuce [D8] Commandes drush utiles

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 :

Features

Fonction Commande
Exporter un nouveau composant dans une feature drush cex
Importer les configurations du site drush cim

Modules

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

Base de données

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)

Autres

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

Astuce [D8] Ajouter un mode d'affichage à un nœud

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 :

Activation du mode d'affichage

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.

Astuce [D8] Créer un bloc

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é.

Déclaration du bloc

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 :

  • La classe hérite de BlockBase, fournie par Drupal.
  • Des annotations permettent de préciser le nom, le nom technique et le groupe (= catégorie) du bloc.
  • La méthode principale est build(), qui retourne un tableau de theming Drupal pouvant être rendu à l'écran.
  • Les deux autres permettent d'ajouter un champ au formulaire de configuration de bloc natif fourni par Drupal.

Remarque :

Le tableau de theming définit un cache par utilisateur (#cache), et le code HTML (#markup) constituant le contenu de votre bloc.

Activation du bloc

Pour que Drupal trouve votre nouveau bloc, vous devez vider les caches.

  • Allez ensuite en back-office, dans Structure > Administration des blocs
  • Dans la région qui contiendra le bloc, cliquez sur Placer le bloc
  • Trouvez votre module dans la liste qui apparaît et cliquez sur Positionner le bloc

Positionner le bloc

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 ?.

Affichage

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.

Astuce [D8] Créer un module

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.

.info

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 package permet de regrouper les modules sur la page de liste des modules du back-office. Vous pouvez réutiliser le même package que celui d'un module existant.
  • La version est celle du module, généralement en deux partie, celle de la version du cœur et celle du module.

.module

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).

Architecture

Votre module contiendra probablement par la suite deux répertoires principaux :

  • src : répertoire contenant la grande majorité de votre code PHP, sous forme de classes d'objet
  • templates : répertoire contenant les templates fournis par votre module

Résultat

Une fois fait, vous devriez voir votre module en back-office :

Créer un module

Vous pouvez l'activer via cette interface ou utiliser drush :

drush en mymodule -y

Astuce [D8] Créer un contenu programmatiquement

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 :

  • Les arguments de la méthode create() permettent de définir le type de nœud, son créateur, son statut, sa langue, ...
  • Tous les champs sont ensuite valués via la méthode set().

Astuce [D8] Theming : définir une nouvelle apparence pour un élément

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).

Habillage à utiliser

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,
);

Déclaration de l'habillage

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 :

  • La fonction définit un nouvel habillage my_hello.
  • Le template à utiliser est my_hello_box.html.twig.
  • La variable who sera transmise au template, avec la valeur 'World' par défaut.

Template

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.

Erreur Impossible d'importer ou de télécharger les traductions de l'interface

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.

Astuce Changer l'utilisateur d'Apache

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.

Astuce Monter automatiquement un disque partagé au démarrage

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).

Astuce Monter automatiquement un disque partagé au démarrage

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).

Erreur L'interface eth1 n'est pas lancée au démarrage de la VM

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

Erreur [D7] Les thumbnails générés ne s'affichent pas

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 :

https://www.drupal.org/node/244924

Astuce Connaître l'espace disque restant

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).

Astuce Connaître la liste des services accessibles via le conteneur de services

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

Astuce Réinitialiser le mot de passe root de MySQL sous Windows

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 :

  • Modifiez la requête SQL pour définir le mot de passe que vous souhaitez utiliser
  • La commande mysqld doit recevoir 2 paramètres valués :
    • Le fichier de configuration de MySQL à utiliser (prendre celui déjà existant)
    • Le fichier SQL d'initialisation à lancer au démarrage (c'est lui qui contient votre fameuse requête)

Astuce Vérifier la configuration d'Apache avec WampServer

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 : Vérifier la configuration d'Apache avec WampServer

Astuce [D7] Créer un webservice REST

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.

URI

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 :

  • On souhaite que les URL de tous les webservices commencent par api/.
  • On définit 2 URL, une pour lire un article et l'autre pour le mettre à jour.
  • Les webservices seront implémentés dans les fonctions my_module_ws_article_read() et my_module_ws_article_update() du fichier my_module.ws.inc.
  • Les deux webservices devront recevoir un nid valide en paramètre dans l'URL.

Page callback

Webservice en lecture

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 :

  • On définit le type de requête attendue par le webservice (requêtes GET ou OPTIONS depuis n'importe quel origine), ainsi que le format de sortie (ici JSON).
  • On récupère le nœud dont le nid est en paramètre. S'il est valide, on prépare le rendu de l'affichage par défaut du nœud.
  • On récupère le titre de l'article depuis le nœud, et le rendu de son champ content depuis la vue.
  • On stocke le tout dans un tableau.
  • On transforme le tableau en JSON que l'on affiche.

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.

Webservice en écriture

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 :

  • On récupère le corps de la requête qui doit contenir les informations de mise à jour au format JSON. Exemple :
{
    "title": "Mon article",
    "content": "<p>Le contenu HTML de mon article</p>"
}
  • On vérifie que le JSON est valide et contient toutes les données.
  • On met à jour l'article

Remarques :

  • Il serait judicieux d'ajouter des logs dans ce webservice, en cas d'erreur d'une part, mais également en cas de réussite, puisqu'un contenu a été modifié et qu'il y a eu écriture dans la base.
  • L'accès à tout webservice en écriture devrait également être protégé. Dans l'exemple, on autorise toutes les requêtes quelle que soit leur origine. Il est important d'ajouter un filtrage sur les IP autorisées à appeler ce webservice, par exemple dans la configuration d'Apache, ou mieux, une authentification préliminaire (ex: oAuth).

Astuce [D7] Créer un script drush

Drush propose déjà pas mal de fonctionnalités. Il est en plus extensible. Voici comme ajouter votre propre script.

Déclaration du 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 :

  • Le nom du script est say_hello. La fonction appelée derrière sera donc drush_my_module_say_hello().
  • Le script attend un prénom en argument, et éventuellement un signe de ponctuation en option.
  • Dans cet exemple, l'alias correspond aux initiales de My_Module_Say_Hello. On peut appeler le script avec ou sans cet alias :
drush say_hello
drush mmsh

Implémentation du script

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 :

  • On vérifie la valeur en argument
  • On récupère une éventuelle option
  • On effectue le traitement souhaité (ici, on dit bonjour)
  • On affiche le temps qu'a duré le script

Astuce Restreindre l'accès à un répertoire avec Apache

Pour restreindre l'accès à un répertoire ou à un site entier, vous pouvez le protéger par un mot de passe :

Authentification HTTP

Prérequis

Modules Apache

Pour pouvoir utiliser cette fonctionnalité d'Apache, vous aurez besoin des modules suivants : mod_auth_basic, mod_authn_file et mod_authz_user.

Génération du fichier de mots de passe

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 :

  • Vous par exemple créer le fichier passwords dans /usr/local/apache/passwd/.
  • Pour ajouter un autre utilisateur, utilisez la même commande sans l'option -c.

Le fichier généré pourra ressembler à a ça :

user1:.G.h/4RfP93fd
user2:RlPRITNDHefEpl 

Configuration de base

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 :

  • Ce virtualhost définit des règles différentes selon les dossiers du site :
    • La racine du site (mon_site/) et ses sous-répertoires sont accessibles à tous
    • Le répertoire protected/ lui, est protégé par un mot de passe
  • L'authentification basique est utilisée.
  • On indique le fichier contenant les utilisateurs et leurs mots de passe cryptés, autorisés à accéder au répertoire.

Remarque :

Pour plus de sécurité, il est également judicieux de passer le site en HTTPS.

Des groupes d'utilisateurs

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.

Astuce Rediriger vers le site mobile

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.

Cas basique

Imaginons que vous ayez deux virtualhost basiques, un pour chaque version.

Site desktop

<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>

Site mobile

<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 :

  • Si le module de réécriture d'Apache est présent, on l'active
  • Si le nom du navigateur (HTTP_USER_AGENT) contient la chaîne mobi, on applique la règle de redirection, vers le site mobile

Remarque :

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>

Règles supplémentaires

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 :

  1. Les iPad ayant une résolution importante, il peut être préférable de conserver la version desktop pour leurs utilisateurs. Vous pouvez également ajouter d'autres tablettes : !ipad|tablet [NC]. Plus d'informations sur le site Mozilla.
  2. Si vous avez un serveur apache en frontal, qui gère les deux noms de domaine (mon-site.com et m.mon-site.com) dans un seul virtualhost, vous aurez besoin de cette ligne pour éviter une boucle de redirection infinie.
  3. Si votre site desktop fournit des webservices à votre site mobile qui les consomme en AJAX, il ne faut pas que les requêtes en question soient redirigées.

Laisser le choix à l'utilisateur

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>

Erreur [D7] Le nouvel alias d'URL n'est pas pris en compte

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.

  • Vous créez un contenu et vous laissez pathauto générer son alias URL par défaut.
  • Ensuite, vous modifiez le contenu et saisissez un alias personnalisé.
  • Et pourtant, le contenu a toujours l'URL automatique.

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.

Astuce [D7] Ajouter des pages de configuration

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 :

Menu - page de configuration en BO Page de configuration en BO

Voici les différentes étapes pour ajouter une page de configuration.

hook_menu()

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 :

  • La première déclaration permet définir le premier élément de menu (Mon site).
  • Les deux suivantes définissent les sous-éléments de ce menu (Général et Webservices).
  • Les deux formulaires de configuration seront définis respectivement dans les fonctions de callback my_module_admin_form_site_general() et my_module_admin_form_site_ws().
  • Ces fonctions seront recherchées dans le fichier my_module.pages.inc.
  • Pour accéder à ces page, il faudra avoir la permission administer mysite.

Form callback

// 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 :

  • Le fieldset correspond à celui HTML, il permet de regrouper des champs.
  • L'imbrication d'un champ dans un fieldset est reproduite dans le tableau php $form.
  • L'enregistrement du formulaire est géré automatiquement par Drupal.
  • Vous gérez seulement la valeur par défaut des champs (= valeur présente dans la variable correspondante, récupérée via 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, ...

Astuce [D7] Ajouter du JS à la fin du body

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 :

  • le fichier .info de votre thème (mon_theme.info)
  • la fonction 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.

Ajout de la variable au template html.tpl.php

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.

Ajout de javascript avec le scope 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')
  );
}

Astuce Configurer un site en HTTPS

Prérequis

Pour pouvoir utiliser le SSL/TLS et passer votre site en HTTPS, les paquets suivants doivent être installés sur votre serveur :

  • openssl : API de chiffrement
  • mod_ssl : Module apache

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.

Générer un certificat auto-signé

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

Configuration Apache

httpd.conf

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.

Virtualhost

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.

Forcer le HTTPS

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>

Astuce Utiliser l'API Google Map

Voici une liste d'utilisations courantes de l'API Google Map :

  • Créer une carte
  • Afficher des points (= marqueurs)
  • Afficher un point avec un marqueur personnalisé
  • Afficher une bulle au clic sur un marqueur (= InfoWindow)

Google Key API

Pour pouvoir utiliser l'API Google, vous devez tout d'abord avoir un compte Google et générer une clé.

  • Rendez-vous à cette adresse et connectez-vous à votre compte Google
  • Créez un nouveau projet en cliquant sur Continuer
  • Renseignez les noms de domaines qui auront le droit d'utiliser la clé (ex: *.mon-site.com, 127.0.0.1, localhost, ...)
  • Copiez la clé de l'API :

    Copier la clé d'API Google Map

Une carte basique

Google Map basique

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 :

  • Le code HTML ne contient qu'un élément div, dans lequel la carte sera générée en javascript. Cet élément doit avoir une largeur et une hauteur (cf. CSS).
  • On inclue la lib javascript de Google, en lui passant la clé générée précédemment.
  • Le code générant la carte ne sera exécuté qu'une fois toute la page chargée
  • Une fois le script chargé, la fonction initMap() est appelée en callback.
  • Pour initialiser la carte, il faut fournir au moins deux paramètres : les coordonnées du point central de la carte (latitude/longitude), et le niveau du zoom à appliquer.

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>

Afficher des marqueurs

Afficher des marqueurs

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 :

  • On initialise un tableau contenant la liste des marqueurs, avec leurs coordonnées (latitude/longitude) et un libellé à afficher au survol (title).
  • On parcourt ce tableau et on instancie des marqueurs, rattachés à la carte créée précédemment.

Remarque :

Pour centrer automatiquement la carte sur ces points, vous pouvez suivre cet article.

Afficher un marqueur personnalisé

Il est agréable d'utiliser ses propres icônes de marqueur.

Afficher un marqueur personnalisé

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.

Afficher une bulle d'information

Afficher des marqueurs

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 :

  • On stocke du HTML dans une variable javascript.
  • On instancie un objet InfoWindow avec pour contenu ce code HTML.
  • On ajoute un écouteur pour l'évènement clic sur le marqueur, pour que l'InfoWindow s'ouvre.

Astuce Centrer automatiquement une carte en fonction des points affichés

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.

Carte par défaut

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.

Limiter la carte

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 :

  • On instancie un objet limites (= LatLngBounds) et on lui transmet les coordonnées de tous les marqueurs affichés.
  • On demande ensuite à la carte de respecter ces limites.

On obtient alors ce résultat :

Centrage de la carte avec 3 points

Si on enlève le 3ème marqueur, la carte est automatiquement recentrée :

Centrage de la carte avec 2 points

Inconvénient

Le problème de cette technique, c'est que si vous n'affichez qu'un seul point, le zoom sera très important :

Centrage de la carte avec 1 point

Zoom maximal fixe

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
});

Inconvénient

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.

Zoom calculé

À 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 :

  • On ajoute un écouteur sur l'évènement bounds_changed fourni par Google Map.
  • L'écouteur est supprimé afin de laisser l'utilisateur zoomer/dézoomer à sa guise.
  • Quand les limites de la carte sont modifiées, le zoom est à nouveau calculé pour ne pas être supérieur à la valeur souhaitée.

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.

Astuce Générateurs DPE/GES

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) :

Générateurs DPE/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/.

Astuce Utiliser les media queries en JS

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.

Astuce [D7] Afficher le numéro de version du site en back-office

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 :

Version du site en BO

hook_field_widget_form_alter()

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 :

  • On ajoute un champ de type textfield, avec pour libellé Version, en lecture seule (= avec l'attribut HTML readonly).
  • La valeur du champ sera une variable drupal ayant pour nom 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.

Astuce [D7] hook_update()

Description

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.

Application

Hook

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.

Mise à jour du module

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

Mises à jours successives

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.

Astuce Introduction aux micro-données

Principe

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 :

Recherche Google

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.

Application

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 :

  • Pour annoncer que le paragraphe va contenir une adresse postale, on lui ajoute l'attribut 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.
  • Pour détailler les différentes parties de l'adresse, on les délimite via une balise HTML (span ici par exemple) à laquelle on ajoute l'attribut itemprop contenant le nom de la propriété correspondante.
  • Certaines propriétés sont obligatoires et doivent être définies même si on ne veut pas les voir à l'écran. Pour les ajouter au code HTML sans les afficher (et sans CSS particulier), on utilise une balise meta autofermante, dont le contenu est spécifié via l'attribut content.

Bonus

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.

Astuce Centrer verticalement en CSS

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 :

Centrer verticalement en CSS

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 largeur
  • width: 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.

Astuce Sauvegarder/Importer une base de données PostgreSQL

Se connecter

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

Sauvegarder

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 :

  • l'hôte utilisé est localhost
  • le port utilisé est 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.

Importer

La commande d'import est similaire à celle de dump :

psql -U username -h localhost dbname < path/to/dump.sql

Par défaut :

  • l'hôte utilisé est localhost
  • le port utilisé est 5432

Erreur Apache sert de vieux fichiers statiques

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.

Astuce Transformer une VM du format vdi vers vmdk

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.

Astuce Connaitre sa version de Linux

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.

Erreur java.lang.IllegalStateException

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;
}

Astuce Screen : le multiplexeur de terminaux

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.

Installation

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)

Utilisation

Voici un cas d'utilisation classique de Screen.

  • Vous accédez à votre serveur en SSH depuis votre machine A au boulot.
  • Vous tapez plusieurs commandes dont une partie connectée avec l'utilisateur user1, et l'autre avec user2.
  • Vous quittez le boulot et rentrez chez vous. Vous souhaitez récupérer sur votre machine B votre console telle que laissée en quittant la machine A.

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.

Bonus

  1. Pour fermer le terminal courant : CTRL + D. Si c'était le seul, cela quittera screen.
  2. Pour naviguer rapidement entre les deux derniers terminaux utilisés : CTRL + A, puis A
  3. Pour pouvoir scroller dans votre terminal lorsque tout n'est pas affiché : CTRL + A, puis ECHAP Vous pouvez maintenant naviguer avec les flèches et pageUp/pageDown.
  4. Pour se connecter à un screen encore attaché : screen -x ma_session
  5. Pour supprimer une session, connectez-vous-y et tapez exit
  6. Pour forcer une session à se détacher (par exemple si vous avez fermé votre terminal sans avoir détaché votre session) : screen -d ma_session
  7. Pour renommer une session : CTRL + Apuis tapez :nom_session new_nom_session et validez avec Entrée
  8. Pour personnaliser l'apparence de screen, et même lancer/récupérer automatiquement un screen dès votre connexion, suivez cet article et ses commentaires.

Astuce Personnaliser l'apparence de l'invite de commande

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 :

Invite de commande personnalisé

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, ...

Astuce Impossible de trouver mon nouveau skin dans JCMS

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 :

  • Choisissez un espace de travail en back-offfice
  • Allez dans l'Espace d'administration fonctionnelle > Types de publication > Portlet
  • Éditez [Abstract] Portlet Skinable.
  • Cochez les skins à activer dans l'espace de travail

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.

Astuce Autoriser plusieurs soumissions de formulaire

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.

Astuce Initialiser une variable de classe

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");
    }
}

Astuce Créer un lien symbolique entre deux répertoires sous Windows

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 :

  • Menu démarrer
  • Exécuter...
  • Tapez cmd et validez

Si 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 :

  • Selon le lien que vous souhaitez créer, vous devrez peut-être être admin de votre machine (ou lancer l'invite de commande en mode administrateur).
  • Si vous voulez créer un lien vers un répertoire qui n'existe pas (encore) sur votre système, utilisez l'option /D et non pas /J. Cela créera un lien symbolique et non pas une jonction.

Astuce Ajouter un espace (ou autre) tous les n caractères

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;
}

Astuce Afficher une erreur dans JCMS

Dans JCMS (7 et +) les erreurs sont affichées dans un bloc ressemblant à ça.

Affichage d'une erreur dans JCMS

Transmettre le message

Vous devez tout d'abord transmettre votre message à JCMS.

  • Si vous êtes dans une JSP, utilisez l'une ou l'autre de ces méthodes :
// 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);
  • Pour faire la même chose dans une classe Java, utilisez l'une de celles-ci :
    // 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.

Afficher le message

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.

Astuce Ordre des champs en ExtraData

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 :

  • champ2
  • champ3
  • dernierchamp
  • premierchamp

Erreur DomPDF : Affichage des images et prise en compte des feuilles de styles

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);
    }
  }

Marque-page Créer des graphiques en PHP

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 :

Galerie JpGraph

Astuce Tester si une chaîne est vide ou ne contient que des espaces

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.

Astuce [D7] Effectuer des requêtes sur une autre base de données

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');

Astuce [D7] Activer automatiquement tous les modules d'un profil

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')

Marque-page Un décompilateur Java dans Eclipse

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 :

  • Cliquez sur Help > Install New Software... dans le menu d'Eclipse
  • Ajoutez un nouveau site via le bouton Add...
  • Choisissez un nom (ex: JDEclipse-Realign) et cette URL : http://mchr3k-eclipse.appspot.com/
  • Cochez JavaDecompiler Eclipse Plug-in > JD-Eclipse (Realign Edition) et suivez la procédure d'installation

Une fois installé et Eclipse redémarré, vérifiez l'association des fichiers :

  • Windows > Preferences > General > Editors > File Associations
  • Pour *.class without source sélectionnez Class File Editor par défaut

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.

Astuce [D7] Ajouter un mode d'affichage à un noeud

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 :

Activation du mode d'affichage

Astuce [D7] Ajouter des variables au template node.tpl.php

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 :

  • On vérfie le type de nœud, pour n'effectuer le traitement que pour les articles
  • La requête récupère les nids des nœuds recherchés. Ils sont ensuite chargés.
  • Les nœuds sont envoyés en paramètre au template. La variable $content['last_published_articles'] sera disponible dans le template.

Remarque :

  • En général on ajoute une condition sur le type d’affichage (Contenu complet, Accroche, ...) disponible via la variable $view_mode, pour éviter d'effectuer le traitement là où c'est inutile.

Astuce [D7] Créer un bloc

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.

hook_block_info()

/**
 * 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 :

  • Ce hook retourne la liste des blocs à définir, avec pour chacun d'eux, son nom, le type de cache à utiliser, ... (Cf. Documentation)
  • Vous pouvez ajouter autant de blocs que vous le souhaitez dans le tableau de résultats.

hook_block_view()

/**
 * 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 :

  • Ce hook reçoit le nom d'un bloc en argument et retourne son contenu sous forme de tableau de rendu. (Cf. Documentation)
  • Le tableau retourné doit au moins contenir la clé content, avec du texte simple ou du html en valeur.
  • Souvent, chaque case effectue une ou plusieurs requêtes en base pour récupérer des données, puis prépare le texte à afficher.

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;

Astuce [D7] Créer un module

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.

.info

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 package permet de regrouper les modules sur la page de liste des modules du back-office. Vous pouvez réutiliser le même package que celui d'un module existant.
  • La version est celle du module, généralement en deux parties, celle de la version du cœur de Drupal et celle du module
  • Cf. documentation officielle

.module

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.

Résultat

Une fois fait, et après vidage des caches, vous devriez voir votre module en back-office :

Nouveau module en BO

Astuce [D7] Fonctions utiles

String

t($text, $params = array(), $options = array())

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));

Documentation

Images

image_style_url($style_name, $path)

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']
);

Documentation

URL

l($text, $path, $options = array())

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']
);

Documentation

file_create_url($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']);

Documentation

url($path = NULL, $options = array())

Description :

Génère une URL interne ou externe, à partir d'une URL relative ou absolue.

Exemple :

url('node/' . $node->nid)

Documentation

drupal_get_path_alias($path = NULL, $path_language = NULL)

Description :

Retourne l'alias d'URL pour la page avec le chemin en argument

Exemple :

drupal_get_path_alias('node/' . $node->nid)

Documentation

Objets et tableaux

element_children(&$elements, $sort = FALSE)

Description :

Retourne le tableau en entrée, sans les valeurs dont la clé commence par #.

Documentation

Développement

dpm($variable)

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)

Documentation

dpq($select)

Description :

Affiche la requête finale. (Cette fonction est fournie par le module devel)

Documentation

Astuce [D7] Hook après édition/suppression de contenu, multilingue ou non

Drupal fournit 3 hooks pour effectuer des traitements après création/édition/suppression de contenu.

Pour un nœud

À la création

/**
 * Implements hook_node_insert().
 */
function my_module_node_insert($node) {

  // Do something
}

À l'édition

/**
 * Implements hook_node_update().
 */
function my_module_node_update($node) {

  // Do something
}

À la suppression

/**
 * Implements hook_node_delete().
 */
function my_module_node_delete($node) {

  // Do something
}

Pour une entité multilingue

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.

À la création

/**
 * Implements hook_entity_translation_insert().
 */
function my_module_entity_translation_insert($entity_type, $entity, $translation, $values = array()) {

  // Do something
}

À l'édition

/**
 * Implements hook_entity_translation_update().
 */
function my_module_entity_translation_update($entity_type, $entity, $translation, $values = array()) {

  // Do something
}

À la suppression

/**
 * Implements hook_entity_translation_delete().
 */
function my_module_entity_translation_delete($entity_type, $entity, $langcode) {

  // Do something
}

Astuce [D7] Effectuer des traitements en masse

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;
}

Astuce Géocoder une adresse et récupérer les limites d'une ville

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 :

  • Cette classe a été créée initialement pour récupérer les limites de toutes les villes de France. Elle permet toutefois de construire l'appel Google pour géocoder n'importe quelle adresse.
  • La classe permet de gérer un compte client Google Map, permettant de dépasser les 2000 requête/jours. La classe a été réécrite pour l'API Google Map v3. Cette fonctionnalité n'a pas été retestée dans cette version.

Astuce [D7] Remplir des champs de contenu programmatiquement

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é).

Champs multivalués

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';

Champs multilingues

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';

Champs texte

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';

Champs entier/décimal

Même principe pour les champs de type nombre :

$node->field_number[LANGUAGE_NONE][0]['value'] = 42;

Champs booléen

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;

Champs image

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');

Champs lien

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'),  
);

Champs référence entre entités (entityref)

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;

Champs adresse

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',
);

Champs coordonnées - Bounds

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';

Champs métadonnées

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.

Astuce [D7] Trier les résultats d'une requête de manière aléatoire

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é.

Astuce [D7] Rediriger après la connexion

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).

Astuce Utiliser la réflexivité

Instancier un nouvel objet d'une classe

/**
 * 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);
}

Appeler une méthode d'une classe

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.

Instancier un tableau d'objets d'une classe

int size = 3;
Objet[] publicationArray = (Publication[]) java.lang.reflect.Array.newInstance(Publication.class, size);

Astuce Savoir si un case à cocher est cochée

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 !');
    }
});

Erreur $facebook->getUser() always returns 0

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';

Marque-page [eZ5] Les répertoires dans le cœur d'eZ Publish 5

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/.

Astuce [eZ5] Rechercher des contenus par mot clé

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 :

  • Ces méthodes peuvent être utilisées dans un contrôleur, où l'attribut $container (ContainerInterface) est disponible.
  • Tous vos tags doivent avoir une racine commune (= une seule arborescence). Cette racine servira de base pour les recherches.
  • Dans la première méthode, la variables $rootTagID (identifiant du tag racine) est en dur et devrait être récupérée depuis un fichier de configuration.
  • Dans cet exemple, la recherche ne fonctionne pas pour les synonymes. La deuxième méthode peut être améliorée pour les gérer.

Astuce [eZ5] Ajouter des filtres et des fonctions à Twig

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) }}

Prérequis

  • Vous avez déjà créé le Bundle 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.

Création de l'extension Twig

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 :

  • La classe MyExtension étend la classe Twig_Extension fournie par Symfony.
  • La méthode getName() retourne le nom de votre choix pour votre extension.
  • Les méthodes getFilters() et getFunctions() retournent la liste des filtres et des fonctions à ajouter à Twig.
  • Le nom du filtre ou de la méthode est défini par la clé dans le tableau (ici truncate et current_uri).
  • Pour instancier un nouveau filtre, on utilise new \Twig_Filter_Method($this, 'méthode_à_appeler').
  • Et de la même manière, pour une nouvelle fonction new \Twig_Function_Method($this, 'méthode_à_appeler').
  • Les deux dernières méthodes sont celles appelées dans les constructeurs. Elles contiennent le code métier qui effectue le traitement.

Informer Symfony

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 :

  • Chaque paramètre et chaque service du fichier a un identifiant unique (ex : acme_my.twig_extension).
  • On définit la classe MyExtension comme paramètre. Si on déplace ou renomme la classe par la suite, seul le paramètre sera à changer.
  • On déclare l'extension Twig en tant que service, en spécifiant la classe à utiliser et l'argument à passer au constructeur.
  • On tague le service avec twig.extension pour que Symfony sache de quel type de service il s'agit.

Astuce [eZ5] Créer une page de login

Objectif

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.

Prérequis

  • Vous avez déjà créé le Bundle Acme/MyBundle, et l'avez activé dans le fichier ezpublish/EzPublishKernel.php.
  • Le pagelayout utilisé par défaut est AcmeMyBundle::pagelayout.html.twig. Il possède un bloc nommé col_main.
  • Dans le pagelayout, la variable redirect_uri doit être définie et contenir l'url courante (ex: /Ma-rubrique/Mon-article).

Création du template

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 :

  • Ligne 2, on commence par étendre le pagelayout par défaut pour modifier le bloc principal.
  • Juste en dessous, en commentaire, on liste les paramètres disponibles/nécessaires dans le template.
  • Ensuite, on commence la surcharge du bloc principal de la page : col_main.
  • On affiche un message d'erreur si les identifiants sont mauvais ({% if fail_login %}).
  • On affiche un formulaire avec un champ identifiant et un champ mot de passe.
  • Le formulaire pointe vers la route acme_my_user_login, sur laquelle Symfony va brancher le futur contrôleur. L'URL courante lui sera transmise en argument.

Remarques :

  • L'image d'erreur doit être présente dans le répertoire Resources/public/images/ du bundle.
  • Ce template utilise le framework CSS Bootstrap 3.
  • Les textes devraient être entre les tags {% trans %} et {% endtrans %}, pour pouvoir être traduits facilement par la suite si besoin.

Création du contrôleur

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 :

  • Le contrôleur est une classe PHP qui étend la classe Controller du Bundle eZ\Bundle\EzPublishCoreBundle fourni par eZ Publish.
  • Ses deux premières méthodes sont des actions et leurs noms finissent d'ailleurs par Action. Elles retournent un objet Response.
  • La troisième est une méthode utilitaire pour connecter un utilisateur.
  • Par défaut, 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
    )
);
  • Si le formulaire est soumis et valide, on appelle la méthode connectUser() qui connecte l'utilisateur et on fournit une réponse de type redirection.

Configuration de la route

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 :

  • Une route doit être nommée par un identifiant unique de votre choix (ex : acme_my_user_login). C'est lui qui est appelé dans la fonction path() côté template.
  • Elle associe un pattern d'URL, à l'action d'un contrôleur.
  • Pour la première route, si on appelle la page /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.
  • Les éléments dans 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.

Astuce Modifier la locale utilisée dans les JSTL

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>

Astuce Placer une boîte de dialogue au niveau d'un bouton

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 :

  • Au clic sur le bouton, l'élément avec l'ID `my-dialog-confirm``` s'ouvre en fenêtre de dialogue de confirmation, avec les boutons Oui et Non.
  • Après l'ouverture de la pop-in, on la positionne au centre de l'écran en largeur, et la centre en hauteur au niveau du bouton.

Erreur Server sent unexpected return value (409 Conflict) in response to PROPPATCH

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! 

Astuce Afficher les retours à la ligne en JSTL

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" %>

Astuce Mettre à jour les dépendances avec Composer

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 :

  • Pour modifier les dépendances, éditez le fichier composer.json.
  • Il ne faut pas confondre :
    • 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.

Astuce Rediriger une page depuis une iframe

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.

Astuce Chiffrer et déchiffrer une chaîne en Java

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 :

  • La chaîne est (dé)chiffrée grâce à la bibliothèque Cipher de Java, en utilisant la technique de chiffrement Blowfish.
  • Le suffixe en argument est ensuite concaténée à la chaîne chiffrée.
  • La chaîne résultante est finalement convertie en base64.

Remarques :

  • La clé de chiffrement ne doit pas excéder 12 caractères, ou des paramètres doivent être changés dans la JVM.
  • Ces méthodes utilisent la bibliothèque org.apache.commons.codec, qui n'est pas forcément disponible dans le JDK.
  • Le suffixe permet de gérer l'expiration du chiffrement. Par exemple, pour une validité de 24h, il suffit de passer la date au format yyyyMMdd en argument.
  • La conversion en base64 n'est pas indispensable mais permet d'obtenir une chaîne de caractères plus simple et exploitable dans une URL par exemple.
  • 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;
    }

Astuce Créer un arbre à partir d'un tableau d'objets

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 :

  • La première boucle crée un tableau d'objets standards ayant chacun 4 attributs : l'id d'un nœud, celui de son parent, le nœud en question et la liste des nœuds enfants (vide pour l'instant).
  • La deuxième boucle remplit l'attribut children de tous ces objets standards. On a alors la liste de tous les nœud avec pour chacun d'eux ses enfants.
  • La dernière boucle "accroche" les nœuds les uns aux autres pour former l'arbre.

Remarques :

  • A l'instar de la classe Object en java, il existe la classe stdClass (Standard Class) pour représenter des objets.
  • Il est possible de caster un tableau en objet stdClass et vice-versa.

Astuce Rétablir le mot de passe admin par défaut

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 :

  • Remplacez la valeur de stamp par la même valeur que votre dernière ligne de store, en l'incrémentant.
  • Faites de même pour la date de modification (mdate).

JPlatform 10 (merci Axel pour cette MAJ)

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" />

Astuce Éviter les flottements en CSS avec clear

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 :

  • Supprimer le float du premier bloc
  • Définir des largeurs au deux éléments, pour qu'ils ne rentrent pas côte à côte.
  • Utiliser la propriété clear: left.

La propriété clear peut prendre principalement 4 valeurs : none (par défaut), left, right et both.

  • Si la valeur sur un bloc est à left, et qu'un flottant se trouve à sa gauche, le bloc ira à la ligne.
  • Si la valeur sur un bloc est à right, et qu'un flottant se trouve à sa droite, le bloc ira à la ligne.
  • Si la valeur sur un bloc est à both, et qu'un flottant se trouve à sa droite ou à sa gauche, le bloc ira à la ligne.
  • Si la valeur sur un bloc est à 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.

  • Sans clear: left;, il s'affiche à côté d'eux.
  • Avec clear: left;, il s'affiche en dessous.

Astuce [eZ5] Créer un bundle

Qu'est-ce qu'un Bundle ?

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é.

Création du Bundle

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 :

  • Le paramètre namespace est le nom du namespace et le nom du Bundle concaténés.
  • Le nom de domaine peut être le nom du projet, celui de l’entreprise, ... Il peut contenir des /.
  • Le nom du Bundle doit finir par Bundle.

Nom du 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.

Emplacement du Bundle

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.

Format de la configuration

Choisissez yml comme format de configuration, et validez avec Entrée.

Génération et autres configurations

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.

Fichiers générés

L’assistant de création de Bundle a généré l’arborescence suivante :

Arborescence bundle généré

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.

Erreur La méthode parseInt() de Javascript et les nombres commençant par 0

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

Astuce Scroller automatiquement jusqu'à un élément

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 :

  • JQuery doit être inclus dans la page.
  • Depuis jQuery 1.9, la propriété browser a été retirée et transférée dans le plugin jQuery Migrate. Il faut donc ajouter ce plugin pour pouvoir l'utiliser.

Marque-page Afficher du JSON avec Firefox

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 :

Afficher du JSON

Marque-page Timepicker

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.

Premier Datepicker

C'est ce que propose ce site : http://trentrichardson.com/examples/timepicker/.

Premier 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.

Second Datepicker

Si vous utilisez le framework Bootstrap, ce site propose un autre timepicker un peu plus agréable à utiliser : http://jdewit.github.io/bootstrap-timepicker/.

Second timepicker

Erreur [eZ5] FatalErrorException: Error: Class 'XSLTProcessor' not found

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.

Erreur [eZ5] The extension "ext/fileinfo" must be loaded in order for this class to work

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.

Marque-page [eZ] Créer un datatype

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.

Marque-page Documenter et tester ses webservices avec Swagger

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.

Swagger UI

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.

Erreur Impossible de mapper des sous-classes avec JSON

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;
}

Astuce Savoir quel bouton radio est sélectionné

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 :

  • Cette fonction suppose que le bouton radio "Oui" a pour valeur 1 et l'autre 0.
  • Cette méthode peut être utilisée pour une liste de radio plus importante. Il suffit de modifier le test element.val() == 1 par une autre valeur que 1.

Astuce Redimensionner automatiquement la hauteur d'une iframe

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.

Marque-page Tester des webservices avec Firefox

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.

REST Client

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.

Astuce Simuler le readonly sur un bouton checkbox

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 :

  • La checkbox étant disabled, sa valeur ne sera pas envoyée par le navigateur à la page cible du formulaire.
  • Pour parer à ça, on la couple a un input de type hidden, avec le même attribut name et la même valeur. C'est lui qui enverra la donnée.

Astuce Afficher la valeur de tous les champs via la méthode toString()

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.

Astuce Installer PEAR pour PHP et WampServer

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.

Installation

Récupération de l'exécutable

Si le fichier go-pear.bat n'est pas présent dans le répertoire de PHP de WampServer :

  • Téléchargez le fichier go-pear.phar (http://pear.php.net/go-pear.phar).
  • Copiez le fichier dans le répertoire de PHP de WampServer (ex : D:\Dev\wamp\bin\php\php5.3.13\).

Exécution de l'installeur

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.

Configuration de l'environnement

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.)

Mise à jour de PEAR

Pour mettre à jour votre installation, et vérifier que PEAR est bien installé, exécutez la commande suivante :

pear channel-update pear.php.net

Astuce Installer la librairie SVN pour PHP

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.

Installation

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

Classe à corriger

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 :).

Astuce Utiliser Google Analytics pour un Intranet

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']);

Astuce Le logger dans JCMS : log4j

Par défaut, JCMS utilise log4j pour gestionnaire de logs.

Utilisation générale de log4j

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 :

  • Déclarez un nouvel attribut de classe statique qui utilise la méthode getLogger() de log4j.
  • Passez votre classe en argument de cette méthode.
  • Utilisez le logger en appelant ses méthodes debug(), info(), warn(), error() ou fatal(), avec le message en argument, plus éventuellement une exception.

Configuration de log4j

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 :

  • L'attribut name définit pour quels packages utiliser log4j. (Les sous-packages seront automatiquement logués.)
  • L'attribut 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).
  • Les <appender> sont les sorties à utiliser : fichier de log, console, ...
  • Vous pouvez déclarer autant de logger que vous le souhaitez dans le fichier log4j.xml.

Astuce La taille des fichiers uploadés via PHP

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.

Astuce Récupérer la version antérieure d'une Data, dans un DataController

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");

Astuce Les variables disponibles dans le contexte, pour les DataController

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.

Astuce Utiliser un logger dans les classes de test

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 :

  • Déclarez le logger dans votre classe de test.
  • Créez la méthode statique suite(), pour pouvoir initialiser des éléments au début des tests.
  • Utilisez la méthode LoggerFactory.getLogger(), pour initialiser le logger.
  • Utilisez le logger comme avec log4j (ex : 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.

Astuce Méthode d'initialisation au début de la classe de test

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.

Astuce Vider toutes les tables de la base de données Derby

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.

Astuce Utiliser les Singletons en Java avec du multithreading

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;
}

Astuce [eZ5] Développez en mode dev

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.

Erreur [eZ5] L'arbre des contenus n'est plus disponible dans le back-office

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.

Astuce Convertir un fichier en UTF-8 avec ou sans BOM

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.

Astuce Le tag @see de la Javadoc

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.

Astuce [eZ4] Les préférences utilisateur

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.)

Créer/modifier une préférence

Côté template ou HTML

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.

Côté PHP

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.

Récupérer la valeur de la préférence

Côté template

{ezpreference( 'my_preference_name' )}

Explication :

L'utilisateur courant est automatiquement utilisé.

Côté PHP

Il faut utiliser la méthode value() de la classe eZPreferences :

eZPreferences::value( 'my_preference_name' );

Remarques :

  • Pour récupérer la valeur pour un autre utilisateur que celui connecté, utilisez le second argument facultatif.
  • La méthode values() de la classe eZPreferences permet de récupérer toutes les préférences d'un utilisateur.

Astuce [eZ4] Différences entre les méthodes variable() et variableArray() de la classe eZINI

La classe eZINI propose deux méthodes pour récupérer des variables sous forme de tableau : variable() et variableArray().

variable()

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
  ...
)

variableArray()

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
  ...
)

Marque-page Utiliser l'API Google Analytics en PHP

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']);

Astuce Utiliser l'API Google Analytics en PHP

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']);

Astuce Rendre son site responsive

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.

Côté HTML

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" />

Côté CSS, les media queries

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 :

  • On ajoute un niveau d'imbrication à la feuille css en ajoutant des blocs media query.
  • Dans chacun de ces blocs on définit les propriétés css à appliquer uniquement si la condition est vraie.
  • Les conditions utilisées dans ces media queries définissent une largeur maxi et/ou mini d'écran. (D'autres conditions sont possibles.)

Remarques :

  • IE 8 et inférieur ne reconnaissent pas les media queries, ainsi que Firefox 3. La librairie Respond.js permet alors de les simuler.
  • Vous pouvez également définir ces conditions de media queries directement dans votre head HTML, via l'attribut media de la balise link.

Un article complet sur les media queries est présent sur le site alsacréations.

Astuce Modifier le nombre de colonnes disponibles dans Bootstrap

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.

Astuce Utiliser les flottants en CSS

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.

Exemple de layout en colonnes

Code HTML

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.

Code CSS

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 :

  • Le bloc de navigation est positionné à gauche grâce à la propriété float: left.
  • Le bloc de contenu est bien positionné à droite (et pas en dessous), grâce à la propriété overflow: hidden.
  • La marge entre les deux est définie sur le bloc de navigation car c'est lui qui est flottant.
  • Le bloc de navigation peut flotter par rapport au bloc de contenu, car il est écrit AVANT, dans le code HTML.

Astuce Forcer le rendu du navigateur d'Internet Explorer et Google Chrome

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" />

Astuce Utiliser l'autocomplétion de l'invite de commande Windows

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.

Astuce Afficher les tâches planifiées

Sous Linux, pour savoir les tâches planifiées pour l'utilisateur courant, utilisez la commande suivante :

crontab -l

Marque-page Propriétés CSS interprêtées par les logiciels de messagerie et webmail

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/

Erreur Erreur au démarrage de JCMS : lucene

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 :

User Entries

Astuce La méthode getStringArrayProperty() de la classe Channel

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 :

  • Le séparateur par défaut est l'espace (ou tabulation et autres chaînes d'espace)
  • Pour modifier le séparateur, il faut l'indiquer au début de la liste de valeurs, avec un @.
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.

Astuce [eZ5] Générer la configuration Symfony du projet existant

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 :

  • Symfony permet de switcher simplement entre les environnements de production et de développement. Pour chacun d'eux elle propose un fichier de configuration par défaut : ezpublish/config/ezpublish_dev.yml et ezpublish/config/ezpublish_prod.yml.
    Remplacez --env=prod par --env=dev pour utiliser la configuration de développement.
  • Le groupe de siteaccess est une nouvelle notion introduite par eZ Publish 5.

Astuce Ouvrir un jalios:link dans une nouvelle fenêtre

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.

Astuce [eZ5] Les liens symboliques dans eZ Publish 5

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 :

  • La première commande crée des liens symboliques dans le répertoire web/, pointant vers les ressources des bundles.
  • La seconde crée des liens pointant vers les ressources du répertoire 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.

Astuce Désactiver un module JCMS

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.

Astuce Simplifier l'utilisation des id dans les fichier de propriétés

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.

Astuce [eZ4] Utiliser l'API Ajax d'eZ Publish

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 principe

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.

PHP

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;
    }
}

Configuration

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.

Un premier test

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.

Javascript

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 :

  • L'exemple présenté utilise jQuery. Vous pouvez également utiliser l'API YUI fournie avec eZ.
  • Le premier élément du tableau (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 :

  • Si une erreur se produit, le message est disponible dans la variable data.error_text.
  • Si l'appel réussit, le résultat est présent dans la variable data.content.

Astuce [eZ4] Utiliser les alias de fetch

Pour simplifier l'utilisation des fetch dans les templates, eZ Publish propose d'utiliser des alias.

Exemple d'utilisation

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 :

  • L'alias s'appelle children_article_count()
  • Il n'a qu'un seul paramètre : l'ID du nœud parent.

Configuration

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 :

  • La section (children_article_count) est le nom de votre alias, à utiliser dans votre template.
  • Le module et la fonction sont ceux que vous auriez appelés dans le fetch standard.
  • Les constantes sont les paramètres fixes que vous auriez passés au fetch standard.
  • Les paramètres permettent de mapper le nom des paramètres de l'alias avec ceux du fetch standard.

Remarques :

  • N'oubliez pas de vider les caches pour qu'eZ Publish prennent en compte cette configuration.
  • EZ Publish fournit déjà des alias, visibles dans le fichier settings/fetchalias.ini.

Astuce Automatiser le nom des dumps avec phpMyAdmin

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.

Astuce [eZ4] Créer ses fetch personnalisés

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.

Le module

  • Commencez par créer un nouveau module (ou utilisez un module déjà existant dans votre extension).
  • Si c'est un nouveau, vous devez le déclarer dans le fichier module.ini.

Par exemple, dans le fichier monextension/settings/module.ini.append.php :

<?php /* #?ini charset="utf-8"?

[ModuleSettings]
ExtensionRepositories[]=monextension
ModuleList[]=monmodule

Le PHP

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 :

  • Cette fonction s'appelle tree_unique
  • Elle est de type lecture (elle n'effectue pas de modifications sur les données, mais en retourne)
  • Elle retourne le résultat de la méthode fetchObjectTree() de la classe ATContentFunctionCollection
  • Elle a un paramètre obligatoire : l'ID du nœud parent

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.

Dans les templates

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 ) 
)}

Astuce Autoriser une machine distante à accéder à un serveur web

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

Astuce Autoriser un serveur distant à se connecter à une base MySQL

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 :

  • Dans cet exemple, l'autorisation d'accès concerne toutes les tables de la base my_db.
  • Le mot nom d'utilisateur et le mot de passe sont ceux de l'utilisateur de la base de données à laquelle on pourra se connecter.
  • Le serveur est la machine souhaitant se connecter à la base. Cela peut être son IP ou son nom.

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;

Astuce Connaître les modules chargés par Apache

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.

Erreur Erreur au lancement d'Apache

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

Marque-page Utiliser les assertions avec PHP

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 :

  • Pour valider les arguments passés à une fonction non publique. On peut ainsi éviter de tester les paramètres d'une fonction private appelée dans une boucle, par exemple.
  • Pour valider des post-conditions. Par exemple, pour vérifier que l'objet nouvellement créé à bien un ID.

Tutoriel pour utiliser les assertions avec PHP : http://openclassrooms.com/courses/les-assertions-en-php.

Astuce L'EntityManager de Doctrine pour Symfony

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).

persist($entity)

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);

flush()

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();

clear($nomEntity = null)

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();

detach($entity)

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();

contains($entity)

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

refresh($entity)

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 »

remove($entity)

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();

Astuce Utiliser Doctrine avec la console de Symfony

Voici un récapitulatif des commandes de base de la console de Symfony pour Doctrine :

  • Créer une base de données :
php bin/console doctrine:database:create
  • Créer/mettre à jour le schéma des tables dans la base :
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.

  • Générer le code d'une entité :
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.

  • Compléter le code d'une entité (champs, getter() et setter()) :
php bin/console doctrine:generate:entities BlogBundle:Article

À adapter en fonction de l'entité à mettre à jour.

Marque-page Les deux php.ini de WampServer

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.

Erreur Impossible de générer l'autoloads pour les dépendances de Symfony avec Composer

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

Erreur Impossible de télécharger les dépendances avec Composer et Git

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

Astuce Créer un Bundle

Pour créer un Bundle avec Symfony, utilisez la commande :

php app/console generate:bundle
  • Choisissez si votre bundle sera commun à plusieurs applications ou non
  • Saisissez le nom de votre bundle, finissant par Bundle (ex: MonBundle). Si vous avez mis oui à l'étape précédente, préfixez le d'un namespace (ex: MonSite\MonBundle).
  • Laissez le répertoire cible src/ inchangé.
  • Choisissez le format de configuration.

Si vous préférez le créer à la main, voici ce qu'il faut retenir pour le rendre opérationnel :

  • Le code source se trouve dans src/MonBundle/
  • Le seul fichier obligatoire doit être à sa racine : MonBundle.php.
  • Vous devez instancier votre bundle dans le noyau pour qu'il soit chargé. Pour cela, ajoutez la ligne new MonBundle\MonBundle(), dans AppKernel.php.
  • Pour que vos routes soient reconnues, il faut ajouter ces lignes au fichier app/config.routing.yml :
mon_bundle:
    resource: "@MonBundle/Controller/"
    type:     annotation
    prefix:   /
  • Pour que vos services soient reconnus, il faut ajouter cette ligne au fichier app/config.yml :
    imports:
    # [...]
    - { resource: "@MonBundle/Resources/config/services.yml" }

Astuce Optimiser l'affichage des animations javascript

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.

Astuce Utiliser l'ajax-refresh pour recharger un élément du DOM

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
});
  • L'id est celui de l'élément dont le contenu doit être rechargé (innerHTML).
  • L'url est le chemin vers la jsp (et pas jspf) qui génère le contenu.
  • 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");

Marque-page [eZ4] Récupérer les urls et les chemins vers les répertoires

EZ Publish fournit la classe eZSys, propose pas mal de méthodes pour récupérer par exemple :

  • Le chemin vers le répertoire var
  • L'url du serveur
  • Le port utilisé
  • Le chemin vers le répertoire du projet
  • La version de php
  • ...

Cette classe se trouve dans lib/ezutils/classes/ezsys.php.

Astuce Ne pas compiler les .less à chaque démarrage

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

Marque-page [eZ4] Un système de chat

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.

Interface de l'extension 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.

Astuce [eZ4] Un système de forums élaboré

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 :

  • une interface dans le back-office pour administrer les modérateurs, les rangs et les paramètres des forums.
  • une nouvelle classe de contenu Forums, qui permet de regrouper dans un même affichage tous les forums enfants.
  • une interface front-office de messagerie privée et d'ajout de contacts

L'extension distribuée sur le repo svn est mal internationalisée et mal traduite. Voici la même version corrigée.

Astuce [eZ4] Configuration Apache et PHP pour eZ Publish avec WampServer

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 Apache
  • httpd-vhosts.conf : Fichier de configuration apache pour les hôtes virtuels
  • php.ini : Configuration de php

Remarque :

Dans cet exemple, la version 2.2 64 bits de WampServer est utilisée, avec Apache 2.2.22 et PHP 5.3.13.

Configuration finale

  • Url du site : http://mon_site.loc/index.php
  • Chemin absolu vers le projet : D:\Dev\php_projects\MonSite\
  • Chemin absolu vers WampServer : D:\Dev\wamp\

Httpd.conf

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.

Modules Apache

Via l'interface de Wamp, activez les modules Apache suivants :

Modules Apache activés

Hôtes virtuels

É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.

Php.ini

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.

Extensions PHP

Comme pour les modules Apache, activez ces extensions PHP via l'interface de WampServer :

Extensions de PHP activées

Astuce Ajouter des variables d'environnement

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'

Erreur javac/Main introuvable au lancement de la webapp

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 :

  • Clic-droit sur le projet
  • Run As > Run Configurations...
  • Onglet Classpath
  • Cliquez sur User Entries puis sur le bouton Advanced...
  • Sélectionnez Add Variable String:
  • Copiez-collez le chemin vers le fichier tools.jar qui se trouve dans le répertoire lib/ de votre jdk (ex: D:\Dev\DevPack\jdk\lib\tools.jar)

Astuce Choisir le portail d'affichage d'une publication

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.

Astuce Modifier le chemin par défaut de l'invite de commande Windows

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.

Solution badasse

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%.

Solution pacifique

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.

Erreur [eZ4] Les fichiers sont importés à la racine et pas dans la médiathèque

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
*/ ?>

Astuce Démarrer un nouveau projet avec JCMS

Cet article explique comment démarrer un nouveau projet JCMS 7.X avec un développement en modules synchronisés.

Environnement

  • Commencez par récupérer et déployer un devpack JCMS. ()Par exemple, extraire le devpack dans D:\Dev).
  • Lancez eclipse via le lanceur eclipse.bat.

Création du projet

  • Créez un nouveau projet Java avec la configuration par défaut.
  • Récupérez et extrayez une webapp JCMS 7.X vierge à la racine de ce nouveau projet.
  • Clic-droit > Refresh sur le projet.

Configuration du projet

Il faut préciser à eclipse quelles classes compiler et avec quelles bibliothèques.

  • Clic-droit > Properties sur le projet
  • Java Build Path > Source :
    • Supprimez le dossier src/ existant, et ajoutez le dossier WEB-INF/classes/.
    • Choisissez ce même dossier pour qu'y soient générées les .class (default output folder).
  • Java Build Path > Libraries > Add Library... > User Libraries... :
    • Créez une bibliothèque Tomcat6 (elle sera réutilisable pour tous vos futurs projets) :
      • Cliquez sur New..., nommez votre bibliothèque Tomcat6 et validez.
      • Cliquez sur Add JARs... et parcourez l'arborescence jusqu'au dossier tomcat6/lib/ de votre Devpack.
      • Sélectionnez les fichiers ecj-3.7.jar, el-api.jar, jasper.jar, jasper-el.jar, jsp-api.jar, servlet-api.jar et validez.
    • De la même manière créez la bibliothèque MonProjet, en sélectionnant cette fois tous les fichiers présents dans le dossier WEB-INF/lib/ de JCMS.
    • Cochez maintenant ces deux bibliothèques et validez, pour qu'elles soient ajoutées à votre projet.
  • Validez les propriétés de votre projet.

Lancement de l'application JCMS

Pour pouvoir lancer l'application, vous devez indiquer le paramétrage de tomcat et les configurations d'exécution d'eclipse.

  • Copiez le fichier MonProjet.launch à la racine de votre projet.
  • Dans ce fichier, remplacez MonProjet par le nom de votre projet, et D:/Dev/DevPack/ par le chemin vers votre devpack.
  • Si votre workspace ou les dossiers jdk16/ et tomcat6/ ne se trouvent pas à la racine de votre devpack, modifiez les chemins correspondants en conséquence.
  • Éditez le fichier 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>
  • Clic-droit > Refresh sur le projet
  • Cliquez sur la petite flèche près du bouton Démarrer JCMS et cliquez sur MonProjet.
  • Accédez au site à l'url http://localhost:8080/jcms, (/jcms étant le contexte choisi dans le tomcat.xml).

Astuce [eZ4] Priorité de surcharge des fichiers .ini

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.

Où trouver les fichiers .ini ?

  • Toujours dans un répertoire settings.
  • Dans le répertoire settings/ à la racine du projet ou dans ceux des extensions.

Différence en les fichiers .ini et .ini.append.php

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.

La hiérarchie de surcharge

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.

Priorité entre les extensions

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.

Astuce Grouper des points sur une Google Map

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 : Exemple de Marker clustering

À 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.

Astuce Choisir le premier champ non nul dans une requête SQL

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.

Astuce [eZ4] Afficher le contenu d'une variable

Pour connaitre le contenu d'une variable dans eZ Publish il existe plusieurs pour faciliter le débogage.

Côté PHP

La fonction print_r()

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

print_r()

La fonction var_dump()

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

print_r()

La fonction eZDebug::writeDebug()

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

print_r()

Dans un template

L'opérateur attribute()

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

print_r()

{$variable|attribute( 'show', 2 )}

affichera

print_r()

La fonction debug-log

Cette fonction fait la même chose que eZDebug::writeDebug(), mais côté template.

{debug-log var=$variable}

affichera

print_r()

Astuce Débugger en PHP avec XDebug

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 :

Début phpinfo()

Plus bas dans la page, l'encadré suivant devrait apparaître si XDebug est activé :

XDebug phpinfo()

Installation de XDebug pour WampServer

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 :

  • Éditez votre fichier php.ini (icône Wampserver > PHP > php.ini)
  • Modifiez ainsi les lignes de la section 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
  • Enregistrez le fichier et redémarrez WampServer.

Installation de XDebug sous Debian

  • Commencez par installer le paquet XDebug
sudo apt-get install php5-xdebug
  • Éditez le fichier de configuration de XDebug :
sudo gedit /etc/php5/conf.d/xdebug.ini
  • Modifiez ainsi les lignes de la section XDEBUG Extension 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
  • Sauvegardez le fichier et redémarrez Apache :
sudo /etc/init.d/apache2 restart

Module easy Xdebug pour Firefox

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 Activer Easy XDebug pour l'activer. Pour le désactiver, cliquez sur Désactiver Easy XDebug

Configuration d’Eclipse pour XDebug

Il s’agit maintenant d’indiquer à Eclipse que vous souhaitez utiliser XDebug pour déboguer votre projet :

  • Menu Window > Preferences
  • Sélectionnez l’item PHP > Debug
  • À la ligne PHP Debugger choisissez XDebug

Config XDebug Netbeans

  • Cliquez sur le lien configure... de XDebug
  • Sélectionnez XDebug dans la liste Installed Debuggers
  • Bouton Configure
  • Dans la liste Accept remote session (JIT), choisissez any
  • Cliquez deux fois sur OK

Configuration de NetBeans pour XDebug

  • Clic-droit sur votre projet > Properties
  • Cliquez sur Run configuration dans la barre de gauche
  • Remplissez les champs demandés :
    • Run as : Local Web Site
    • Project URL : l'url de la page d'accueil de votre site (ex : http://monsite.com)
    • Index File : le fichier index de votre site (a priori index.php)
  • Cliquez sur le bouton Advanced...
  • Sélectionnez Do Not Open Web Browser
  • Cliquez deux fois sur OK

Activer le débogueur pour NetBeans

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.

Astuce [eZ4] Créer un lien de téléchargement d'un fichier

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.

Astuce [eZ4] Consommer des services web fournis par Play!

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 :

  • Cette classe étend la classe ATWebservices également fournie par l'extension.
  • Chaque méthode ne fait que préparer le début d'une requête HTTP, avec le nom du service web et les paramètres à envoyer.
  • Ces requêtes HTTP sont co