PHP Objet

  • Depuis PHP5, le langage intégre totalement les concepts objets
  • Notion de classe et d'instance de classe
  • Méthodes et paramètre implicite $this
  • Constructeurs et autre méthodes magiques
  • Héritage et abstraction
  • Clonage
  • Exceptions
  • ...

Définition d'une classe

  • On utilise le mot-clé class
  • Les variables membres d'une classe sont des variables déclarées avec le mot-clé indiquant sa visibilité public, protected ou private
  • L'opérateur d'accès est -> (comme en C++)
  • Les méthodes sont des fonctions définies à l'intérieur de la classe
  • La propriété de visibilité s'applique aussi aux méthodes qui sont publiques par défaut.
  • On accède aux variables d'instance à l'aide de $this et de l'opérateur ->

Exemple

<?php
class De {
  // face visible du dé (propriété publique)
  public int $faceVisible;
  // nombre de faces du dé (propriété privée)
  private int $nbFaces;

  // méthode publique
  public function afficheFaceVisible() : void {
    echo $this->faceVisible;
  }
}
?>

Constructeur et Instanciation

  • Les constructeurs sont définies via une méthode magique __construct()
  • L'opérateur new associé à un appel à un constructeur permet de creer une nouvelle instance de classe (un objet)
  • Exemple :
    <?php
    class De {
      private int $nbFaces;
      public int $faceVisible;
    
      // constructeur
      public function __construct(int $nbFaces=6) {
        $this->nbFaces = $nbFaces;
        $this->faceVisible = rand(1, 6);
      }
      public function getFaceVisible() : int {
        return $this->faceVisible;
      }
    }
    // script de test
    $instanceDe = new De();
    $instanceDe->getFaceVisible();
    print_r($instanceDe);
    ?>
  • Résultat :
    2
    De Object ( [faceVisible] => 2 [nbFaces:De:private] => 6 ) 

Méthodes magiques

  • méthodes préfixées par __ (double underscore) et réservées par PHP
  • __construct() : constructeur
  • __destruct() : destructeur
  • __toString() : méthode destinée à convertir automatiquement en string un objet
  • __clone() : méthode décrivant les modalités de clonage d'un objet
  • et bien d'autres...

Variables et méthodes de classe

  • Le mot-clé static permet de définir des variables et des méthodes de classe
  • Ces éléments sont accessibles sans instance (propriétés liées à la classe)
  • Possibilité de définir une visibilité spécifique
  • On accède aux éléments statiques via le nom de la classe et l'opérateur de résolution de portée ::
  • Exemple :
    <?php
    class De {
      public static int $nbInstances = 0;
      public int $faceVisible;
      private int $nbFaces;
    
      public function __construct(int $nbFaces=6) {
        self::$nbInstances++;
        $this->nbFaces = $nbFaces;
        $this->faceVisible = rand(1, 6);
      }
    
      public static function getNbInstances(): int {
        return self::$nbInstances;
      }
    }
    
    echo De::getNbInstances(); // affiche 0
    $d= new De();
    echo De::getNbInstances(); // affiche 1
    
    ?>

Héritage

  • Permet d'étendre le concept véhiculé par une classe
  • Utilise le mot clé extends
  • La classe mère est accessible via parent::
  • Exemple :
    class Depipe extends De {
      private int $valeur;
      public function __construct(int $nbFaces, int $valeur): int {
        parent::__construct($nbFaces); // appel du constructeur de la classe mère
        $this->valeur = $valeur;
        $this->faceVisible = $valeur;
      }
      public function getValeur(): int {
        return $this->valeur;
      }
      public function setValeur(int $valeur): void {
        $this->valeur = $valeur;
        $this->faceVisible = $valeur;
      }
      // surcharge de De::lancerDe
      public function lancerDe(): void {
        $this->faceVisible = $this->valeur;
      }
    }
    

Abstraction et interfaces

  • PHP 5 introduit les classes abstraites et les méthodes abstraites
  • L'abstraction de classe permet de définir des concepts incomplets
  • Le mot-clé abstract permet de faire de telles définitions
  • Rappel n°1 : Une classe abstraite ne peut être instanciée
  • Rappel n°2 : Une méthode abstraite est juste déclarée dans sa classe, sa définition n'apparaît que dans une classe descendante non abstraite (instanciable)
  • Les interfaces permettent de définir un ensemble de méthodes devant être définies par une classe pour assurer un service
  • Le mot-clé interface est destiné à leur définition
  • Toutes les méthodes d'une interface sont publiques

Typage des paramètres

  • Le typage des paramètres des méthodes est partiellement possible en PHP 5.0
  • Ce typage est limité à des noms de classe ou d'interface
  • Exemple :
    require_once("PieceQuarto.php");
    
    class BoitePieceQuarto {
    
      protected $pq;
      public static $compteurVide = 0;
    
      public function __construct(PieceQuarto $pq = null) {
        $this->pq = $pq;
      }
    
  • PHP 5.1 permet d'utiliser le type array
  • PHP 7 étend cette possibillité aux types simples (bool, float, int, string), au typage des valeurs de retours des méthodes et aux fonctions.
  • Exemple :
    
        public function getPieceDispo(int $position) : PieceQuarto {
        return $this->piecesDispos[$position];
      }
      
  • PHP 7.1 accepte le type void et permet de préfixer les autres types de retour avec ? afin d'accepter NULL comme valeur de retour.
  • PHP 7.2 accepte le type générique object.
  • PHP 7.4 les propriétés de classe peuvent être typées, version disponible sur ust-infoserv et utilisée pour tester votre code !.
    class User {
        public int $id;
        public string $name;
    }

Constantes de classe et mot-clé final

  • Il est possible de définir des constantes de classes
  • Il s'agit de valeurs scalaires fixées définitivement et liées au niveau de la classe (comme les variables statiques)
  • Depuis PHP 5.6 (dernière version avant PHP7), les constantes de classe peuvent être associées à des tableaux
  • Exemple :
    class MyClass
    {
        const CONSTANT = 'constant value';
    
        function showConstant() {
            echo  self::CONSTANT . "\n";
        }
    }
    
            echo MyClass::CONSTANT . "\n";
  • Le mot-clé final permet d'interdire la surcharge d'une méthode par une classe descendante ou d'interdire d'appliquer un héritage à une classe

Persistance des données : les sessions

  • Problème : La durée de vie d'une variable PHP est limité à une seule URL.
  • Pourtant : la persitance des données est un mécanisme nécessaire pour développer des applications web
  • Comment transmettre des informations entre différentes pages ?
    • Avec des formulaires et des champs cachées : seulement valable pour des données simples et des volumes limités.
    • Avec des bases de données : mécanisme lourd
    • Avec des données de session...
  • L'identifiant de session est transmis par un cookie.

Création d'une session

  • session_start(): bool (renvoie toujours TRUE)
  • Initialise ou prolonge une session en utilisant un SID (identifiant de session généré automatiquement).
  • La constante SID est une chaîne vide si les cookies sont activés
  • session_start() doit être appel avant tout affichage dans le navigateur (utilise l'en-tête de requête HTTP).
  • Source session1.php :

Les données de la session

  • Accessibles via la superglobale $_SESSION
  • Création d'une variable de session : $_SESSION['nomVariable'] = valeur
  • La fonction bool isset(nom) permet de vérifier si une variable est définie.
  • La fonction void unset(nom) détruit une variable.
  • La fonction void session_unset(void) détruit toutes les variables de session.
    Exemple :Suppression d'une variable de session : unset($_SESSION['nomvariable']

  • Simplification des noms de variables à l'aide des références (à utiliser avec modération) :
    <?php
    session_start();
    $nom = &$_SESSION['nom'];
    ?>
  • Exemple (valable si une session est en cours) :
    <?php
    if (!isset($_SESSION['heure'])) {
        $date = getdate();
        $heure = $date['hours'].":".$date['minutes'];
        $_SESSION['heure']= $heure;
        }
    echo $_SESSION['heure']; ?>

Entretien d'une session

  • Source session2.php :
  • Quelle heure était-il lors de l'accès à la diapo précédente ?
    <?php
    if (isset($_SESSION['heure']))
        echo $_SESSION['heure']; ?>
  • Lors de la lecture de la page précédente il était

Fin de session

  • La fonction session_destroy() détruit toutes les données associées à la session courante.
  • Cette fonction ne détruit pas les variables globales associées à la session, de même, elle ne détruit pas le cookie de session.
  • Avant de clore une session, il faut la réactiver !
  • Source session3.php :
  • Exemple :
    <?php
        session_destroy();
        if (isset($_SESSION['heure']))
            echo 'Les variables de session restent accessibles malgré le session_destroy ! '. $_SESSION['heure'];
        else
            echo "Plus de date en mémoire après le reload.";
    ?>

L'identifiant de session

  • Transmis par cookie ou par paramètre de l'URL.
  • Accès à l'id avec la fonction session_id() ou parfois avec la constante SID
  • Il esr possible d'imposer un identifiant de session :
    • session_id(string $identifiant) remplace l'identifiant généré automatiquement par l'argument.
    • Doit être appelé avant le premier session_start()
    • Ne doit utiliser que des lettres ou des chiffres, la virgule ou le signe moins.

Durée du cache

  • session_cache_expire ( [int new_cache_expire]): int
  • Retourne la valeur courante associée à la durée accordée au cache mémoire (en minutes)
  • Le paramètre optionel permet de redéfinir cette durée (180mn par défaut)
  • S'utilise avec session_cache_limiter(string cache_policy): string pour identifier le limiteur de cache utilisé par le navigateur (none, nocache, private, private_no_expire, public)

A propos du cookie de session

  • session_get_cookie_params(void): array : retourne un tableau contenant les données associé au cookie de la session courante
    • 'lifetime' : durée de vie
    • 'path' : chemin où sont rangées les infos
    • 'domain' : domaine du cookie
    • 'secure' : pour la sécurité des connexions

Agir sur le cookie de session

  • session_set_cookie_params(int lifetime [,string path [,string domain [,bool secure]]]): void
  • Surtout utile pour agir sur la durée de la session.
  • N'est valide que le temps du script où elle est appelée.
  • Doit être appelée avant session_start()

Autres API

  • session_write_close() : mémorise les données de session et termine la session courante.
  • session_encode() : retourne une chaine linéarisée des variables de session
  • session_decode() : décode une chaine et alimente $_SESSION
  • session_abort()
  • ...

Classe Carte.php

Classe BoiteCarte

Classes JeuCartes et Main

Classe PokerUI et script principal

HTTP, REST & Cie

D. Fournier

HTTP, CRUD, REST et MVC

Le Protocole IETF Badge HTTP

  • Origine : inventé avec les URL et HTML par Tim Berners-Lee en 1989-90
  • Web conçu autour de trois piliers :
    • HTTP : le protocole de communication,
    • HTML : le format de document initiant la notion d'hypertexte,
    • URL : le système d'adressage des documents
  • HTTP est un protocole sans état mis à disposition par un serveur httpd
  • Principe de fonctionnement : on émet une requête et on reçoit une réponse <point c'est tout !>.
  • HTTP : les versions du protocole
    • HTTP[/0.9] : version initiale ultra basique avec une seule requête possible GET /fichier.html et qui retourne exclusivement une page HTML
    • HTTP/1.0 : version extensible, introduction des méthodes POST et HEAD, mise en place des en-têtes, gestion des codes MIME (devenus Media type RFC 6838) et retourne un code d'état en plus du document demandé
    • HTTP/1.1 : version standardisée toujours extensible
    • HTTP/2 : version optimisée issue SPDY proposé par Google

Les Requêtes IETF Badge HTTP

  • Les méthodes de requête indiquent l'action que l'on souhaite réaliser sur la ressource indiquée.
  • Les méthodes sont souvent appelées verbes HTTP.
  • Principaux verbes :
    • GET demande une représentation de la ressource spécifiée. Les requêtes GET doivent uniquement être utilisées afin de récupérer des données.
    • POST est utilisée pour envoyer une entité vers la ressource indiquée. Cela entraîne généralement un changement d'état ou des effets de bord sur le serveur.
    • HEAD demande une réponse identique à une requête GET pour laquelle on aura omis le corps de la réponse (on a uniquement l'en-tête).
    • PUT remplace toutes les représentations actuelles de la ressource visée par le contenu de la requête.
    • DELETE supprime la ressource indiquée.
    • Mais aussi CONNECT, OPTIONS, TRACE, PATCH...

Les en-têtes IETF Badge HTTP

  • Elles permettent de préciser des informations en plus des requêtes ou des réponses, il s'agit en quelque sorte de paramètres
  • Syntaxe : nomEntete: valeur
  • Liste détaillée des en-têtes disponibles : RFC4229
  • Plus de détails
  • Exemple d'en-tête de requête :
    Host: tools.ietf.org
    User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:58.0) Gecko/20100101 Firefox/58.0
    Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
    Accept-Language: fr,fr-FR;q=0.8,en-US;q=0.5,en;q=0.3
    Accept-Encoding: gzip, deflate, br
    Referer: http://localhost/~dominique/PHP/coursHTTP.php?page=3
    Connection: keep-alive
    Upgrade-Insecure-Requests: 1
  • Exemple d'en-tête de réponse :
    Date: Sun, 18 Feb 2018 20:17:01 GMT
    Server: Apache/2.2.22 (Debian)
    Content-Location: rfc4229.html
    Vary: negotiate,Accept-Encoding
    TCN: choice
    Last-Modified: Sun, 11 Feb 2018 09:01:19 GMT
    ETag: "3ca206-220af-564ec013715c0;56582426c5330"
    Accept-Ranges: bytes
    Cache-Control: max-age=604800
    Expires: Sun, 25 Feb 2018 20:17:01 GMT
    Content-Encoding: gzip
    Strict-Transport-Security: max-age=3600
    X-Frame-Options: SAMEORIGIN
    X-Xss-Protection: 1; mode=block
    X-Content-Type-Options: nosniff
    X-Clacks-Overhead: GNU Terry Pratchett
    Content-Length: 16953
    Content-Type: text/html; charset=UTF-8

Structure d'un message

  • Les requêtes
    • Un message est constitué d'une requête sur une ligne, puis suivi d'en-têtes sur des lignes séparées.
      schéma d'une requête
    • la présence d'une ligne vide (avec un CRLF) déclenche l'envoie de la requête
    • Exemple :
      $ telnet localhost 80
      Trying 127.0.0.1...
      Connected to localhost.
      Escape character is '^]'.
      HEAD /~dominique/PHP/index.php HTTP/1.0
      Accept-language: fr,en
      Accept-Encoding: utf-8
      
      
  • Les réponses
    • La première ligne de la réponse est composée de la version de protocole utilisé, du code de réponse qui indique l'éventuel bon déroulement de la requête et d'un message de statut de la réponse. Les lignes suivantes sont des en-têtes puis vient la ressource demandée s'il y en a une.
      schéma d'une réponse
    • Exemple :
      HTTP/1.1 200 OK
      Date: Sun, 18 Feb 2018 21:19:05 GMT
      Server: Apache/2.4.18 (Ubuntu)
      Connection: close
      Content-Type: text/html; charset=UTF-8
      
      Connection closed by foreign host.

Code des réponses

  • Ces codes indiquent si la requête s'est déroulée normalement, ils sont codés sur 3 chiffres dont le plus connu est le 404...
  • Il existe 5 classes de codes indiqué par le premier chiffre.
  • Classe 1xx : réponses informatives
    • 100 Continue Jusqu'ici tout va bien...
    • 101 Switching Protocol
    • ...
  • Classe 2xx : réponses de succés
    • 200 OK
    • 201 Created
    • 204 No Content
    • ...
  • Classe 3xx : redirections
    • 301 Moved Permanently
    • 304 Not Modified
    • ...
  • Classe 4xx : erreurs du client
    • 400 Bad Request
    • 401 Unauthorized
    • 403 Forbidden
    • 404 Not Found
    • 405 Method Not Allowed
    • 418 I'm a teapot Voir RFC 2324
    • ...
  • Classe 5xx : erreurs du serveur
    • 500 Internal Server Error
    • 502 Bad Gateway
    • 503 Service Unavailable
    • ...

Générer un en-tête en PHP

  • void header ( string $header [, bool $replace = TRUE [, int $http_response_code ]] )
  • Cette fonction permet de générer des en-têtes http et en particulier dans le cadre des redirections automatiques (détails et RFC HTTP/1.1).
  • Important : Lors d'un header, il est impératif qu'aucun contenu n'ait été envoyé avant.
  • Fichier test/testHeader.php :
     if (isset($_GET['coffee'])) {
        header("HTTP/1.1 418 I'm a teapot");
        echo "\n<!DOCTYPE HTML PUBLIC \"-//IETF//DTD HTML 2.0//EN\">
    <html><head>
    <title>418 I'm a teapot</title>
    </head><body>
    <h1>I'm a teapot</h1>
    <p>The requested URL <b>". $_SERVER['SCRIPT_NAME']."?". $_SERVER['QUERY_STRING']. "</b> tried to brew coffee with a teapot !</p>
    </body></html>";
    }
    else {
        header("HTTP/1.1 301 Moved Permanently");
        if (isset($_GET['page'])) {
            header('Location: /~dominique/PHP/coursHTTP.php?page=' . $_GET['page']);
        }
        else
            header('Location: /~dominique/PHP/coursHTTP.php?page=0');
    }
    
  • Où me mènent ces liens :
  • https://www.google.com/teapot/
  • https://http.cat/418

Retour sur le traitement des formulaires en PHP

GET ou POST ?

Répétions de requêtes

Illustrations des incohérences produites :

Cas 1 : édition et méthode GET

Cas 2 : passage à la méthode POST

Code PHP (version GET)

try {
    if (isset($_GET['action'])) {
        $dbs1 = $dbh->prepare('INSERT INTO nom(nom) VALUES(:nom)');
        $dbs1->bindValue(':nom', $_GET['nom']);
        $dbs1->execute();
    }
    else {
        $dbs1 = $dbh->query('DROP TABLE IF EXISTS nom ');
        $dbs1 = $dbh->query('CREATE TABLE nom (id integer auto_increment PRIMARY KEY, 
                                               nom varchar(25))');
        $dbs1 = $dbh->query("INSERT INTO nom(nom) VALUES ('nom 1'),('nom 2'),('nom 3')");
    }

    $dbs1 = $dbh->query('SELECT * from nom');
    $tab = $dbs1->fetchAll($fetch_style = PDO::FETCH_ASSOC);

    echo "<h1>Contenu de la table nom</h1>";
    echo "<table border='1'>\n";
    $colnames = array_keys($tab[0]);
    echo "<tr>\n";
    foreach ($colnames as $col) {
        echo "<th>" . $col . "</th>";
    }
    echo "</tr>\n";

    foreach ($tab as $ligne) {
        echo "<tr>\n";
        foreach ($ligne as $col) {
            echo "<td>" . $col . "</td>";
        }
        echo "</tr>\n";
    }
    echo "</table>\n";
} catch (PDOException $e) {
    print "<br />Erreur !: " . $e->getMessage() . "<br/>";
    die();
}
echo "<h2>Formulaire utilisant la méthode GET</h2>";
echo "<form action='' method=get>
<input type='text' name='nom' />
<input type='submit' name='action' value='inserer'/>
</form>";

Une solution : scinder actions et états

Code PHP de la page proposant le formulaire

try {
    echo file_get_contents('util/debutSkelHtml5.html');
    if (!isset($_GET['message'])) {
        $dbs1 = $dbh->query('DROP TABLE IF EXISTS nom ');
        $dbs1 = $dbh->query('CREATE TABLE nom (id integer auto_increment PRIMARY KEY, nom varchar(25))');
        $dbs1 = $dbh->query("INSERT INTO nom(nom) VALUES ('nom 1'),('nom 2'),('nom 3')");
    }

    $dbs1 = $dbh->query('SELECT * from nom');
    $tab = $dbs1->fetchAll($fetch_style = PDO::FETCH_ASSOC);

    echo '<h1>Contenu de la table nom</h1>';
    echo '<table border='1'>';
    $colnames = array_keys($tab[0]);
    echo '<tr>';
    foreach ($colnames as $col) {
        echo '<th>' . $col . '</th>';
    }
    echo '</tr>';

    foreach ($tab as $ligne) {
        echo '<tr>';
        foreach ($ligne as $col) {
            echo "<td> $col </td>";
        }
        echo '</tr>';
    }
    echo '</table>\n';

} catch (PDOException $e) {
    print '<br />Erreur !: ' . $e->getMessage() . '<br/>';
    die();
}

echo '<h2>Formulaire utilisant la méthode POST et traitement par script tiers</h2>';
echo "<form action='actionInserer.php' method='post'>
    <input type='text' name='nom' />
    <input type='submit' name='action' value='inserer'/>
    </form>";

echo file_get_contents('util/finSkelHtml5.html');

Code PHP de la page traitant le formulaire

try {
    if (isset($_POST['action'])) {
        $dbs1 = $dbh->prepare('INSERT INTO nom(nom) VALUES(:nom)');
        $dbs1->bindValue(':nom', $_POST['nom']);
        $dbs1->execute();
    }
    header('HTTP/1.1 303 See Other');
    header('Location: insererForm_v3.php?message=Coucou de la part de actionInserer.php');
    exit;
} catch (PDOException $e) {
    print '<br />Erreur !: ' . $e->getMessage() . '<br/>';
    die();
}

Abstraction de l'accès aux SGBD

  • Objectif : unifier les modes d'accès aux bases de données.
  • Vision orientée objet de l'interface PHP/SGBD.
  • le modèle disponible : PDO (PHP Data Object)

L'extension PDO

  • PDO : PHP Data Object.
  • L'extension PDO définit une interface pour accéder à une base de données depuis PHP.
  • Elle permet d'unifier l'exécution des requêtes et la récupération des résultats.
  • Les spécificités des dialectes SQL ne changent pas.
  • Des pilotes spécifiques aux SGBD permettent la connexion aux serveurs.
  • L'extension s'articule autour de deux classes (PDO et PDOStatement) et une exception (PDOException).

Les pilotes disponibles

  • PDO_IBM : IBM DB2
  • PDO_INFORMIX : IBM Informix Dynamic Server
  • PDO_MYSQL : MySQL 3.x/4.x/5.x
  • PDO_ODBC : ODBC v3 (IBM DB2, unixODBC et win32 ODBC, ...)
  • PDO_PGSQL : PostgreSQL
  • PDO_SQLITE : SQLite 3 et SQLite 2
  • PDO_SQLSRV : Microsoft SQL Server / SQL Azure
  • PDO_CUBRID : Cubrid
  • PDO_DBLIB : FreeTDS / Microsoft SQL Server / Sybase
  • PDO_FIREBIRD : Firebird
  • PDO_OCI : Oracle Call Interface

Le constructeur PDO

  • La classe PDO est en charge de la connexion avec le SGBD et de la transmission des requêtes.
  • La connexion à une BD se fait lors de la création d'une instance de PDO.
    public __construct ( string $dsn [, string $username [, string $password [, array $options ]]] )
  • La variable dsn (Data Source Name) précise les informations nécessaire à la connexion à la base de donnée. Il peut s'agir
    • d'une chaîne de caractères répondant aux spécifications du pilote.
    • d'une URI indiquant la localisation d'un fichier contenant la chaîne DSN
    • d'un alias défini dans php.ini
  • DSN correspondant à un pilote :
    $dbh = new PDO('mysql:host=localhost;dbname=dbtest', $user, $pass);
    $dbh = new PDO('pgsql:host=localhost dbname=dbtest user=usertest password=test');
    
  • URI permettant l'accès à un DSN (à éviter...) :
    $dbh = new PDO('uri:http://proba.univ-lehavre.fr/~dominique/PHP/test/dsn.txt');
    dsn.txt
    pgsql:host=localhost dbname=dbtest user=usertest password=test
  • Alias
      $dbh = new PDO('pgdb');
    
    php.ini
    ...
    [PDO]
    pdo.dsn.mydb="mysql:host=localhost;dbname=dbtest"
    pdo.dsn.pgdb="pgsql:host=localhost dbname=dbtest user=usertest password=test"

PDO::query, PDO::exec et PDO::prepare

  • La méthode PDO::query(string $statement [, ...]) exécute une requête SQL et retourne l'éventuel résultat dans un objet PDOStatement.
  • La méthode PDO::exec(string $statement) exécute une requête SQL et retourne le nombre de lignes affectées (utile pour des requêtes de type DML).
  • La méthode PDO::prepare(string $statement [, ...] initialise une requête qui sera exécutée via l'objet PDOStatement retourné. On peut ainsi exécuter plusieurs fois la même requête avec des paramètres différents.
$dbs1 = $dbh->query('SELECT * from Commune');
$nb = $dbh->exec('UPDATE  Commune set com_nbvotants = 832
                    WHERE com_nom=\'Moustan\'');
$dbs2 = $dbh->prepare('SELECT * FROM Commune
                        WHERE com_nom = ? OR com_dept =?');
$dbs3 = $dbh->prepare('SELECT * FROM Commune
                        WHERE com_nom = :commune OR com_dept = :dept');

Les transactions

  • Lors de la réalisation de requêtes d'édition (DML ou DDL), il est parfois nécessaire de s'assurer des résultats avant de valider l'effet des requêtes (commit).
  • bool PDO::beginTransaction() : initie une transaction en désactivant l'auto-commit du SGBD.
  • bool PDO::commit() : valide une transaction et rend effectives les requêtes associées.
  • bool PDO::rollBack() : annule la transaction courante.
  • bool PDO::inTransaction() : véfifie si une transaction est active dans le pilote.
  • Attention : MySQL réalise systématiquement un commit en DDL.

Gestion des erreurs

  • mixed PDO::errorCode() :
    • retourne le SQLSTATE associé à la dernière opération sur la base de donnée.
    • ne renvoie que le code d'erreur des requêtes exécutées directement avec PDO::exec
  • array PDO::errorInfo() : retourne les informations associées à la dernière erreur. Le tableau contient trois informations :
    • Le code erreur SQLSTATE,
    • Le code erreur du pilote,
    • Le message d'erreur du pilote.

Autres méthodes de PDO

  • PDO::lastInsertId([ string $nomSeq=NULL ]) : permet de récupérer l'identifiant du dernier enregistrement inséré ou la dernière valeur générée par la séquence nomSeq.
  • PDO::quote(string $string [, int $parameter_type=PDO::PARAM_STR]) : formate une chaîne destinée à être incorporée dans une requête.
  • PDO::getAttribute(int $attribute) : accède à un attribut de connexion parmi PDO::ATTR_AUTOCOMMIT, PDO::ATTR_CASE, PDO::ATTR_CLIENT_VERSION, ...
  • PDO::setAttribute(int $attribute, mixed $value) : modifieur correspondant.
  • PDO::getAvailableDrivers() : renvoie un tableau listant les pilotes disponibles (méthode statique).

La classe PDOStatement

  • Les instances de cette classe sont associées à la requête préparée et aux résultats obtenus une fois exécutée.

PDOStatement::execute

  • Execute une requête préparée (avec PDO::prepare).
  • Signature : bool PDOStatement::execute ([array $input_parameters=array() ])
  • Exemple :
    $dbs2 = $dbh->prepare('SELECT * FROM Commune
                            WHERE com_nom = ? OR com_dept =?');
    $dbs2->execute(array("Grobourg","76"));
    
    $dbs3 = $dbh->prepare('SELECT * FROM Commune
                            WHERE com_nom = :commune OR com_dept = :dept');
    $dbs3->execute(array(':commune' => 'Grobourg', ':dept' => 'GRD'));
    

PDOStatement::fetch

  • Retourne la ligne suivante du résultat associé à l'objet courant.
  • Signature : mixed PDOStatement::fetch ([ int $fetch_style=PDO::FETCH_BOTH [, int $cursor_orientation=PDO::FETCH_ORI_NEXT [, int $cursor_offset=0 ]]])
  • Le format du résultat dépend du paramètre fetch_style à choisir parmi les constantes : PDO::FETCH_ASSOC, PDO::FETCH_NUM , PDO::FETCH_BOTH (mode par défaut), PDO::FETCH_OBJ, ...
  • Exemple :
    $tabAssocNum = $dbs1->fetch();
    $tabNum = $dbs1->fetch(PDO::FETCH_NUM);
    $tabAssoc = $dbs1->fetch(PDO::FETCH_ASSOC);
    $obj = $dbs1->fetch(PDO::FETCH_OBJ);

fetchAll, fetchObject, et cie

  • array PDOStatement::fetchAll (...) : retourne dans un tableau toutes les lignes d'un résultat (pratique mais augmente la charge du serveur).
  • mixed PDOStatement::fetchObject ([ string $class_name [, array $ctor_args ]]) : retourne un objet de la classe spécifiée en paramètre.
  • mixed PDOStatement::fetchColumn ([ int $column_number=0 ]) : permet d'accéder colonne par colonne aux différentes valeurs d'un enregistrement.
  • bool PDOStatement::setFetchMode ( ... ) : définit le comportement par défaut de fetch.

rowCount et columnCount

  • int PDOStatement::rowCount () : retourne le nombre de lignes obtenues lors du dernier appel de la méthode execute().
  • int PDOStatement::columnCount () : retourne le nombre de colonnes du dernier résultat.

Gestion des erreurs

  • Complète les méthodes de la classe PDO pour les erreurs survenues lors d'opérations réalisées depuis une instance de PDOStatement.
  • string PDOStatement::errorCode() : similaire à la méthode homonyme de PDO
  • array PDOStatement::errorInfo() : idem

Binding de variables

  • Mécanisme permettant de lier des variables PHP à des paramètres de requêtes ou à des colonnes
    • bool PDOStatement::bindParam (mixed $parameter, mixed &$variable [, int $data_type [, int $length [, mixed $driver_options]]]) :
    • bool PDOStatement::bindValue (mixed $parameter, mixed $value [, int $data_type]) :
    • bool PDOStatement::bindColumn (mixed $column, mixed &$param [, int $type[, int $maxlen [, mixed $driverdata]]]) :
    • A noter : les passages de paramètres par références dans bindParam et bindValue (voir dans l'exemple plus loin)
  • bool PDOStatement::debugDumpParams ( ) : détaille une commande préparée directement sur la sortie standard

PDOException

  • Exception lancée exclusivement par PDO
  • Peut être récupérée comme en Java :
    try {
      $dbh = new PDO('pgdb');
      $dbs1 = $dbh->query('SELECT * from Commune');
      ...
    } catch (PDOException $e) {
        print "Erreur !: " . $e->getMessage() . "<br/>";
        die();
    }

API spécifiques

  • PDOStatement::getColumnMeta : uniquement avec Postgres, Mysql, Sqlite et DBlib
  • PDOStatement::getAttribute : spécifique Firebird et ODBC
  • PDOStatement::setAttribute : spécifique Firebird et ODBC

PDOStatement::closeCursor

  • Libère la connexion au serveur
  • Utile pour les drivers qui ne supportent pas que des PDOStatement laissent trainer des lignes non récupérées
  • Peu utile...

PDOStatement::nextRowSet

  • Pour les systèmes qui acceptent les procédures retournant plus d'un result set ou rowset
  • Permet de passer au rowset suivant dans la séquence de résultats

Quelques constantes de l'environnement PDO

Pour les types SQL (entiers)

Pour les fetch

Pour l'environnement

Exemples PDO

  • Les schémas E/A ci-dessous résument la conception de la base de données destinées à gérer cette collection de livres.
  • La base de données LMSF est constituée de 3 relations : livre, auteur et livaut issues du schéma E/A simplifié ci-dessous.
    Schéma E/A des relations livre, auteur et livaut
  • La relation livre contient 4 attributs : liv_num un numéro, liv_titre un titre, liv_depotlegal une date de dépot légal.
  • La relation auteur contient 3 attributs : aut_id un identifiant d'auteur, aut_nom un nom et aut_prenom un prénom.
  • La relation livaut (Ecrit) associe les identifiants des livres et de leurs auteurs.

Affichage du résultat d'un SELECT

Résultat

Affichage du contenu d'une table ou d'une vue :

Sources

if (isset($_POST['table'])) {
  $table = $_POST['table'];

  try {
    include('dsn.php'); // établi une connexion et instancie une variable $dbh

    $dbs1 = $dbh->query('SELECT * from '. $table);

      // récupère un tableau associatif avec le résultat de la requête
    $tab = $dbs1->fetchAll($fetch_style = PDO::FETCH_ASSOC );

    echo "<table border='1'>\n";

    $colnames = array_keys($tab[0]); // pour le nom des colonnes
    echo "<tr>\n";
    foreach ($colnames as $col) {
      echo "<th>".$col."</th>";
    }
    echo "</tr>\n";


    foreach ($tab as $ligne) {
      echo "<tr>\n";
      foreach ($ligne as $col) {
        echo "<td>".$col."</td>";
      }
      echo "</tr>\n";
    }
    echo "</table>\n";

  } catch (PDOException $e) {
    print "<br />Erreur !: " . $e->getMessage() . "<br/>";
    die();
  }
}

Édition de tables


    try {
    include('dsn.php');

    $dbh->exec('INSERT INTO livre VALUES(117, \'La lune disparue\', NULL)');
    $dbh->exec('INSERT INTO livre VALUES(118, \'Retour à jamais\', NULL)');
    $dbh->exec('INSERT INTO livre VALUES(119, \'Le visage du démon\', NULL)');
    $dbh->exec('INSERT INTO livre VALUES(120, \'Programme spécial\', NULL)');

    $nb = $dbh->exec('DELETE FROM livre WHERE liv_num > 116');

    echo "<p>$nb enregistrements ont été supprimés.</p>\n";


    } catch (PDOException $e) {
    print "<br />Erreur !: " . $e->getMessage() . "<br/>";
    die();
    }

Résultat

Requêtes paramétrées (1/2)

   try {
        include 'dsn.php';

        $dbs3 = $dbh->prepare('SELECT * from livre WHERE liv_num = ? OR liv_titre LIKE ?');
        $dbs3->execute(array(100, "Le %"));
        $tab = $dbs3->fetchAll($fetch_style = PDO::FETCH_ASSOC );

        afficheTable($tab);
    } catch (PDOException $e) {
        print "<br />Erreur !: " . $e->getMessage() . "<br/>";
        die();
    }
     ?>
    

Résultat


Requêtes paramétrées (2/2)

Résultat

Affichage des livres écrits par un auteur :


Sources

     <form action="<?php echo($PHP_SELF);?>" method="post">
     <fieldset>
        <legend>Affichage des livres écrits par un auteur :</legend>
        <select name="nom">
        <?php
            include 'dsn.php';

            $dbs = $dbh->query('SELECT aut_nom, aut_prenom FROM auteur ORDER BY aut_nom, aut_prenom');
            while ($auteur = $dbs->fetch()) {
                echo "<option value='".$auteur['aut_nom']."'>".$auteur['aut_nom']." ".$auteur['aut_prenom']."</option>\n";
            }
        ?>
        </select>
        <input type="submit" name="Afficher" value="afficher" /></fieldset>
     </form>

     <?php
        if (isset($_POST['nom'])) {
            $nom = $_POST['nom'];
            try {
                include 'dsn.php';

                $dbs2 = $dbh->prepare('SELECT * from titreauteurs t
                                    WHERE aut_nom1 like :auteur OR aut_nom2 = :auteur');

                $dbs2->execute(array("auteur" => $nom));
                $tab = $dbs2->fetchAll($fetch_style = PDO::FETCH_ASSOC );

                afficheTable($tab);
            } catch (PDOException $e) {
                print "<br />Erreur !: " . $e->getMessage() . "<br/>";
                die();
            }
        }
     ?>
    

Binding de variables

Code source

 try {
    include 'dsn.php';

    $dbs2 = $dbh->prepare('SELECT * from auteur WHERE aut_nom = :auteur');
    $auteur = "Dick";
    $dbs2->bindParam(':auteur', $auteur, PDO::PARAM_STR);
    $dbs2->execute();
    $tab = $dbs2->fetchAll($fetch_style = PDO::FETCH_ASSOC );

    afficheTable($tab);

    $dbs3 = $dbh->prepare('SELECT * from auteur WHERE aut_nom LIKE ? OR aut_prenom LIKE ?');
    $dbs3->bindValue(1, "Dick", PDO::PARAM_STR);
    $dbs3->bindValue(2, "Fr%", PDO::PARAM_STR);
    $dbs3->execute();
    $tab = $dbs3->fetchAll($fetch_style = PDO::FETCH_ASSOC );

    afficheTable($tab);

    $dbs4 = $dbh->prepare('SELECT * from auteur WHERE aut_id < 5');
    $dbs4->execute();
    $dbs4->bindColumn(1, $id);
    $dbs4->bindColumn(2, $nom);
    $dbs4->bindColumn('aut_prenom', $prenom);
    while ($ligne = $dbs4->fetch(PDO::FETCH_BOUND)) {
      echo "<p>". $id . " " . $nom . " " . $prenom . "</p>\n";
    }


  } catch (PDOException $e) {
    print "<br />Erreur !: " . $e->getMessage() . "<br/>";
    die();
        }

Résultats

Gestion d'erreurs

Code source

try {
    include 'dsn.php';

    $dbs = $dbh->query('SELECT aut_ville from auteur WHERE aut_nom = :auteur');
    if ($dbs) {
      $tab = $dbs->fetchAll($fetch_style = PDO::FETCH_ASSOC );
      afficheTable($tab);
    }
    else {
      echo "<p><b>". $dbh->errorCode() . "</b> : ";
      foreach($dbh->errorInfo() as $cle => $val)
	echo $cle."=".$val." ";
      echo "</p>";
    }

  } catch (PDOException $e) {
    print "<br />Erreur !: " . $e->getMessage() . "<br/>";
    die();
    }

Résultat

Objet Métier

Problématique

Associer dans une classe les besoins d'une application et les contraintes provenant du SI. Dans le cas de la base LMSF, on dispose de 4 tables qui dispersent des informations souvent manipulées ensemble, par exemple, la notice d'un livre. Un objet métier devra permettre de faire la synthèse entre la problématique du SI et de l'application, typiquement :

  • permettre l'accès à une donnée métier (en général à l'aide d'une vue SQL),
  • permettre sa création, son édition et sa sauvegarde (qui dépendent souvent de plusieurs tables).

Configuration des paramètres de connexion à la BD


$_ENV['host'] = "ust-infoserv.univlehavre.lan";
$_ENV['user'] = "xy123456";
$_ENV['db'] = "xy123456";
$_ENV['passwdt'] = "***********";
    

Exemple simple : la classe LivreMetier

class LivreMetier {

    /**
     * gestion statique des accès SGBD
     * @var PDO
     */ 
    private static $_pdo;

    /**
     * gestion statique de la requête préparée de selection
     * @var PDOStatement
     */ 
    private static $_pdos_select;

    /**
     * gestion statique de la requête préparée de mise à jour
     *  @var PDOStatement
     */ 
    private static $_pdos_update;

    /**
     * gestion statique de la requête préparée de d'insertion
     * @var PDOStatement
     */ 
    private static $_pdos_insert;

    /**
     * gestion statique de la requête préparée de suppression
     * @var PDOStatement
     */ 
    private static $_pdos_delete;

    /**
     * PreparedStatement associé à un SELECT, calcule le nombre de livres de la table
     * @var PDOStatement;
        */
    private static $_pdos_count;

    /**
     * PreparedStatement associé à un SELECT, récupère tous les livres
     * @var PDOStatement;
        */
     private static $_pdos_selectAll;



    /**
     * Initialisation de la connexion et mémorisation de l'instance PDO dans LivreMetier::$_pdo
     */ 
    public static function initPDO() {
        self::$_pdo = new PDO("mysql:host=".$_ENV['host'].";dbname=".$_ENV['db'],$_ENV['user'],$_ENV['passwd']);
        // pour récupérer aussi les exceptions provenant de PDOStatement
        self::$_pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
    }

    /**
     * préparation de la requête SELECT * FROM livre
     * instantiation de self::$_pdos_selectAll
        */
    public static function initPDOS_selectAll() {
        self::$_pdos_selectAll = self::$_pdo->prepare('SELECT * FROM livre');
    }

     /**
     * méthode statique instanciant LivreMetier::$_pdo_select
     */ 
    public static function initPDOS_select() {
        self::$_pdos_select = self::$_pdo->prepare('SELECT * FROM livre WHERE liv_num= :numero');
    }

    /**
     * méthode statique instanciant LivreMetier::$_pdo_update
     */ 
    public static function initPDOS_update() {
        self::$_pdos_update =  self::$_pdo->prepare('UPDATE livre SET liv_titre=:titre, liv_depotlegal=:depotLegal WHERE liv_num=:numero');
    }

    /**
     * méthode statique instanciant LivreMetier::$_pdo_insert
     */ 
    public static function initPDOS_insert() {
        self::$_pdos_insert = self::$_pdo->prepare('INSERT INTO livre VALUES(:numero,:titre,:depotLegal)');
    }

    /**
     * méthode statique instanciant LivreMetier::$_pdo_delete
     */ 
    public static function initPDOS_delete() {
        self::$_pdos_delete = self::$_pdo->prepare('DELETE FROM livre WHERE liv_num=:numero');
    }

    /**
     * préparation de la requête SELECT COUNT(*) FROM livre
     * instantiation de self::$_pdos_count
        */
    public static function initPDOS_count() {
        if (!isset(self::$_pdo))
            self::initPDO();
        self::$_pdos_count = self::$_pdo->prepare('SELECT COUNT(*) FROM livre');
    }


     /**
     * numéro du livre (identifiant dans la table Livre)
     * @var int
     */ 
    protected $liv_num;

    /**
     * titre du livre
     * @var string
     */ 
    protected $liv_titre;

    /**
     * dépot légal du livre
     *   @var string
     */ 
    protected $liv_depotlegal;

    /**
     * attribut interne pour différencier les nouveaux objets des objets créés côté applicatif de ceux issus du SGBD
     * @var bool
     */ 
    private $nouveau = TRUE;

    /**
     * @return $this->liv_num
     */ 
    public function getLiv_num() : int {
        return $this->liv_num;
    }

    /**
     * @param $liv_num
     */ 
    public function setLiv_num($liv_num): void {
        $this->liv_num=$liv_num;
    }

    /**
     * @return $this->liv_titre
     */ 
    public function getLiv_titre() : string {
        return $this->liv_titre;
    }

    /**
     * @param $liv_titre
     */ 
    public function setLiv_titre($liv_titre): void {
        $this->liv_titre=$liv_titre;
    }

    /**
     * @return $this->liv_depotlegal
     */ 
    public function getLiv_depotlegal() : string {
        return $this->liv_depotlegal;
    }

    /**
     * @param $liv_depotlegal
     */ 
    public function setLiv_depotlegal($liv_depotlegal): void {
        $this->liv_depotlegal=$liv_depotlegal;
    }

    /**
     * @return $this->nouveau
     */ 
    public function getNouveau() : bool {
        return $this->nouveau;
    }

    /**
     * @param $nouveau
     */ 
    public function setNouveau($nouveau): void {
        $this->nouveau=$nouveau;
    }

    /**
     * @return un tableau de tous les LivreMetier
     */ 
    public static function getAll(): array {
        try {
            if (!isset(self::$_pdo))
                self::initPDO();
            if (!isset(self::$_pdos_selectAll))
                self::initPDOS_selectAll();
            self::$_pdos_selectAll->execute();
            // résultat du fetch dans une instance de LivreMetier
            $lesLivres = self::$_pdos_selectAll->fetchAll(PDO::FETCH_CLASS,'LivreMetier');
            return $lesLivres;
        }
        catch (PDOException $e) {
            print($e);
        }
    }


    /**
     * initialisation d'un objet métier à partir d'un enregistrement de livre
     * @param $liv_num un identifiant de livre
     * @return l'instance de LivreMetier associée à $liv_num
     */ 
    public static function initLivreMetier($liv_num) : LivreMetier {
        try {
	        if (!isset(self::$_pdo))
	            self::initPDO();
	        if (!isset(self::$_pdos_select))
	            self::initPDOS_select();
	        self::$_pdos_select->bindValue(':numero',$liv_num);
	        self::$_pdos_select->execute();
        // résultat du fetch dans une instance de LivreMetier
	        $lm = self::$_pdos_select->fetchObject('LivreMetier');
	        if (isset($lm) && ! empty($lm))
	            $lm->setNouveau(FALSE);
	        if (empty($lm))
                throw new Exception("Livre $liv_num inexistant dans la table Livre.\n");
	        return $lm;
        }
        catch (PDOException $e) {
	        print($e);
        }
    }

    /**
     * sauvegarde d'un objet métier
     * soit on insère un nouvel objet
     * soit on le met à jour
     */ 
    public function save() : void {
        if (!isset(self::$_pdo))
            self::initPDO();
        if ($this->nouveau) {
            if (!isset(self::$_pdos_insert)) {
	            self::initPDOS_insert();
            }
            self::$_pdos_insert->bindParam(':numero', $this->liv_num);
            self::$_pdos_insert->bindParam(':titre', $this->liv_titre);
            self::$_pdos_insert->bindParam(':depotLegal', $this->liv_depotlegal);
            self::$_pdos_insert->execute();
            $this->setNouveau(FALSE);
        }
        else {
            if (!isset(self::$_pdos_update))
	            self::initPDOS_update();
            self::$_pdos_update->bindParam(':numero', $this->liv_num);
            self::$_pdos_update->bindParam(':titre', $this->liv_titre);
            self::$_pdos_update->bindParam(':depotLegal', $this->liv_depotlegal);
            self::$_pdos_update->execute();
        }
    }

    /**
     * suppression d'un objet métier
     */ 
    public function delete() :void {
        if (!isset(self::$_pdo))
            self::initPDO();
        if (!$this->nouveau) {
            if (!isset(self::$_pdos_delete)) {
	            self::initPDOS_delete();
            }
            self::$_pdos_delete->bindParam(':numero', $this->liv_num);
            self::$_pdos_delete->execute();
        }
        $this->setNouveau(TRUE);
    }

    /**
     * nombre d'objets metier disponible dans la table
     */
    public static function getNbLivres() : int {
        if (!isset(self::$_pdos_count)) {
            self::initPDOS_count();
        }
        self::$_pdos_count->execute();
        $resu = self::$_pdos_count->fetch();
        return $resu[0];
    }



    /**
     * affichage élémentaire
     */ 
    public function __toString() : string {
        $ch = "<table border='1'><tr><th>liv_num</th><th>liv_titre</th><th>liv_depot_legal</th><th>nouveau</th></tr><tr>";
        $ch.= "<td>".$this->liv_num."</td>";
        $ch.= "<td>".$this->liv_titre."</td>";
        $ch.= "<td>".$this->liv_depotlegal."</td>";
        $ch.= "<td>".$this->nouveau."</td>";
        $ch.= "</tr></table>";
        return $ch;
    }
}
    

Utilisation de la classe LivreMetier

Étape 1 : Récupération du livre numéro 1

$lmsf1 = LivreMetier::initLivreMetier(1);
echo "<p>Livre numéro 1</p>";
echo $lmsf1;
    
Livre numéro 1

"; echo $lmsf1."
"; } catch (Exception $e) { print $e; } ?>

Étape 2 : Création du livre numéro 117

$lmsf117 = new LivreMetier();
$lmsf117->setLiv_num(117);
$lmsf117->setLiv_titre('La lune disparue');
$lmsf117->setNouveau(TRUE);
echo $lmsf117;
setLiv_num(117); $lmsf117->setLiv_titre('La lune disparue'); $lmsf117->setNouveau(TRUE); echo $lmsf117."
"; } catch (Exception $e) { print $e; } ?>

Étape 3 : Sauvegarde du livre numéro 117 dans la BD

$lmsf117->save();
echo $lmsf117;
    
save(); echo "
"; echo $lmsf117."
"; } catch (Exception $e) { print $e; } ?>

Étape 4 : Récupération du livre 117 sauvegardé puis suppression


try {
    $lmsf117bis = LivreMetier::initLivreMetier(117);
    echo "<p>Livre numéro 117 après sauvegarde et récupération</p>";
    echo( $lmsf117bis);

    $lmsf117bis->delete();
    echo "<p>Livre numéro 117 après suppression</p>";
    echo( $lmsf117bis);
    echo "<p>L'instance existe toujours mais n'a plus de pendant coté SGBD</p>";

    echo "<p>Tentative d'initialisation d'un livre inexistant</p>";
    $lmsf117ter = LivreMetier::initLivreMetier(117);
    if (isset($lmsf117))
        echo( $lmsf117ter);
} catch (Exception $e) {
    print $e;
    }
Livre numéro 117 après sauvegarde et récupération

"; echo( $lmsf117bis)."
"; $lmsf117bis->delete(); echo "

Livre numéro 117 après suppression

"; echo( $lmsf117bis)."
"; echo "

L'instance existe toujours mais n'a plus de pendant coté SGBD

"; echo "

Tentative d'initialisation d'un livre inexistant

"; $lmsf117ter = LivreMetier::initLivreMetier(117); if (isset($lmsf117)) echo( $lmsf117ter)."
"; } catch (Exception $e) { print $e."

"; } ?>

Étape 5 : Édition du livre numéro 1

$lmsf1 = LivreMetier::initLivreMetier(1);
echo "<p>Livre numéro 1 avant édition</p>";
echo $lmsf1;
$lmsf1->setLiv_depotlegal('1974-01-01');
$lmsf1->save();
echo "<p>Livre numéro 1 après édition et sauvegarde</p>";
echo $lmsf1;
Livre numéro 1 avant édition

"; echo( $lmsf1); $lmsf1->setLiv_depotlegal('1974-01-01'); $lmsf1->save(); echo "

Livre numéro 1 après édition et sauvegarde

"; echo( $lmsf1); $lmsf1->setLiv_depotlegal(NULL); $lmsf1->save(); echo "
"; } catch (Exception $e) { print $e; } ?>

Standard PHP Library

  • Introduit avec PHP 5
  • Pendant de la STL C++

Interfaces et Classes prédéfinies

  • Traversable
  • Iterator
  • IteratorAggregate
  • ArrayAccess
  • Serializable
  • Closure
  • Generator

Interfaces Traversable et Iterator

  • Traversable : Interface permettant de détecter si une classe peut être parcourue en utilisant foreach. PDOStatement implémente Traversable.
  • Iterator : Interface pour les itérateurs.
    interface Iterator extends Traversable {
        /* Méthodes */
        abstract public mixed current ( void )
        abstract public scalar key ( void )
        abstract public void next ( void )
        abstract public void rewind ( void )
        abstract public boolean valid ( void )
        }
  • Source Classe Livre Iterator
    class LivreIterator implements Iterator, Countable {
    
      protected $idLivre = 1;
    
      public function count() {
        return LivreMetier::getNbLivres();
      }
    
      public function current() {
        return LivreMetier::initLivreMetier($this->idLivre);
      }
    
      public function key() {
        return $this->idLivre;
      }
    
      public function next() {
        $this->idLivre = $this->idLivre+1;
      }
    
      public function rewind (  ) {
        $this->idLivre = 1;
      }
    
      public function valid () {
        return $this->idLivre>0 && $this->idLivre<=$this->count();
      }
          }
  • Exécution exemple :
    $literator = new LivreIterator();
    echo "<h2/>Liste complète</h2>";
    echo "<ul>";
    foreach ($literator as $livre)
        echo "<li>".$literator->key()." = ".$livre->getLiv_num()." ".$literator->current()."</li>";
    echo "</ul>";
    echo "<br/><br/>";
    echo "<h2/>Décalage de 100 livres</h2>";
    $literator->rewind();
    for ($i=0;$i<100;$i++)
        $literator->next();
    echo "<ol>";
    while ($literator->valid()) {
        echo "<li>".$literator->key()." ".$literator->current()."</li>";
        $literator->next();
    }
    echo "</ol>";
    
    echo "<h2/>Focus sur les 7 premiers</h2>";
    $pageCourante = new LimitIterator($literator,0,7);
    
    echo "<ul>";
    foreach ($pageCourante as $val) {
        echo "<li>";
        echo $pageCourante->key()." ".$val;
        echo "</li> ";
    }
    echo "</ul>";

Interface IteratorAggregate

  • IteratorAggregate : Interface pour créer un itérateur externe
    interface IteratorAggregate extends Traversable {
      /* Méthodes */
      abstract public Traversable getIterator ( void )
    }
  • IteratorAggregate permet d'externaliser le mécanisme d'itération (les méthodes d'Iterator)
  • Source classe LivreMetierAggregate :
    class LivreMetierAggregate implements IteratorAggregate {
    
      public function getIterator() {
        return new LivreIterator();
      }
    }
  • Exécution :
    $lma = new LivreMetierAggregate();
    $literator = $lma->getIterator();
    echo "<ul>";
    foreach ($literator as $livre)
        echo "<li>".$literator->current()."</li>";
    echo "</ul>";

Interface ArrayAccess

  • ArrayAccess : Interface permettant d'utiliser les [] sur les objets
    interface ArrayAccess {
      /* Méthodes */
      abstract public boolean offsetExists ( mixed $offset )
      abstract public mixed offsetGet ( mixed $offset )
      abstract public void offsetSet ( mixed $offset , mixed $value )
      abstract public void offsetUnset ( mixed $offset )
    }
    • ArrayAccess::offsetExists — Indique si une position existe dans un tableau
    • ArrayAccess::offsetGet — Position à lire
    • ArrayAccess::offsetSet — Position à assigner
    • ArrayAccess::offsetUnset — Position à supprimer
  • Source LivreIterator2
    class LivreIterator2 extends LivreIterator implements ArrayAccess {
    
      public function offsetExists($offset) {
        $testOffset = LivreMetier::initLivreMetier($offset);
        if (isset($testOffset))
          return true ;
        else
          return false;
      }
    
      public function offsetGet($offset) {
        return LivreMetier::initLivreMetier($offset);
      }
    
      public function offsetSet($offset, $value) {
        // pas possible avec LivreIterator : update de livre ?
      }
    
      public function offsetUnset($offset) {
        // pas possible avec LivreIterator : suppression de livre ?
      }
    
    }
  • Execution exemple :
    $li2 = new LivreIterator2();
    
    echo $li2[5];
    echo $li2[117];
    echo $li2[5];

Serializable, Closure, Generator

  • Serializable : interface permettant de personnaliser la sérialisation à l'aide des méthodes serialize et unserialize (doc PHP)
  • Closure : classe pour gérer les fonctions anonymes (fonctions de rappel ou autres)
  • Generator : itérateurs simplifiés à l'aide de yield...

Contenu de la SPL

  • PHP met à disposition un ensemble de classes et d'interfaces (similaire à la STL de C++)
  • Liste des classes de la SPL : $val) echo $key.", "; ?>
  • Liste des interfaces de la SPL $val) echo $val.", "; ?>

Countable, SeekableIterator, RecursiveIterator

  • Countable : Interface permettant d'utiliser la fonction count() sur les objets
    interface Countable {
    /* Méthodes */
    abstract public int count ( void )
    }
  • SeekableIterator : autorise l'accès à un élément particulier
    /* Méthode supplémentaire*/
    abstract public void seek ( int $position )
  • Source exemple :
    class LivreIterator3 extends LivreIterator implements SeekableIterator {
    
      public function seek($position) {
        if ($position<0 || $position>$this->count()) {
          throw new OutOfBoundsException("position invalide ($position)");
        }
    
        $this->idLivre = $position;
      }
    
    }
  • Execution exemple :
    try {
        $literator3 = new LivreIterator3();
        $literator3->seek(100);
        echo $literator3->current();
        $literator3->seek(200);
    } catch (OutOfBoundsException $e) {
        echo $e->getMessage();
    }
  • RecursiveIterator : Iterator pour structure multi-niveaux, hérite de Itérator
    /* Méthodes supplémentaires */
     public RecursiveIterator getChildren ( void )
    public bool hasChildren ( void )

LimitIterator

  • LimitIterator : Iterator permettant de parcourir un nombre limité d'éléments
  • Source exemple :
    <?php
    //require_once("LivreIterator.php");
    function __autoload($classname) {
        $filename = "./". $classname .".php";
        require($filename);
    }
    session_start();
    
    if (isset($_GET['reset'])) {
      session_unset();
      session_destroy();
    }
    
    if (! isset($_SESSION['collection'])) {
      $_SESSION['collection'] = new LivreIterator();
      $_SESSION['debut'] = 1;
      $_SESSION['taillePage'] = 10;
    }
    
    if(isset($_GET['suivant'])) {
      $_SESSION['debut'] += $_GET['suivant']*$_SESSION['taillePage'];
    }
    
    echo "<p>**** ".count($_SESSION['collection'])." ****</p>";
    echo "<p>**** ".$_SESSION['debut']." -- ".$_SESSION['taillePage']." ****</p>";
    
    $pageCourante = new LimitIterator($_SESSION['collection'],$_SESSION['debut']-1,$_SESSION['taillePage']);
    
    echo "<ul>";
    foreach ($pageCourante as $val) {
      echo "<li>";
      echo $pageCourante->key()." ".$val;
      echo "</li> ";
    }
    echo "</ul>";
    
    $decalageFirst =  (int)(-1 *$_SESSION['debut']/$_SESSION['taillePage']);
    $decalageLast =  (int)((count($_SESSION['collection'])-$_SESSION['debut'])/$_SESSION['taillePage']);
    $decalagePrev = ($_SESSION['debut']==1)?0:-1;
    $decalageNext = ($_SESSION['debut']+$_SESSION['taillePage']>count($_SESSION['collection']))?0:1;
    
    $urlFirst = $_SERVER['PHP_SELF']."?suivant=".$decalageFirst;
    $urlPrev = $_SERVER['PHP_SELF']."?suivant=".$decalagePrev;
    $urlNext = $_SERVER['PHP_SELF']."?suivant=".$decalageNext;
    $urlLast = $_SERVER['PHP_SELF']."?suivant=".$decalageLast;
    
    echo "<p><a href='$urlFirst'>First</a> <a href='$urlPrev'>prev</a> <a href='$urlNext'>next</a> <a href='$urlLast'>Last</a></p>";
    
    $urlReset = $_SERVER['PHP_SELF']."?reset";
    echo "<p><a href='$urlReset'>Reset</a></p>";
    
    ?>
  • Execution exemple : pageLivre.php

IteratorIterator

  • IteratorIterator est un iterateur de base qui permet de transformer en itérateur tout objet Traversable
    • IteratorIterator::__construct(Traversable) — Crée un itérateur à partir d'un objet traversable
    • Traversable IteratorIterator::getInnerIterator() — Retourne l'itérateur interne
  • Attention, un objet Traversable a sans doute de bonnes raisons de ne pas implémenter l'interface Iterator...
  • et bien d'autres itérateurs...

Structures de données

  • SplDoublyLinkedList
  • SplStack
  • SplQueue
  • SplHeap
  • SplMaxHeap
  • SplMinHeap
  • SplPriorityQueue
  • SplFixedArray
  • SplObjectStorage

Exceptions

  • BadFunctionCallException
  • BadMethodCallException
  • DomainException
  • InvalidArgumentException
  • LengthException
  • LogicException
  • OutOfBoundsException
  • OutOfRangeException
  • OverflowException
  • RangeException
  • RuntimeException
  • UnderflowException
  • UnexpectedValueException

Et bien d'autres choses

  • Des Fonctions
    • Réflexives
    • Utilitaires pour les itérateurs
    • d'autochargement de classes avec spl_autoload(), spl_autoload_extensions(), spl_autoload_register(), ...
    • ...
  • Des classes pour gérer des fichiers : SplFileInfo, SplFileObject, SplTmpFileObject
  • Les patterns SplObserver et SplSubject

Sources SPL

Retour sur la persistance des données

  • La Classe LivreMetier vue précédemment présente plusieurs défauts : elle mélange le modèle de données, l'interaction avec la base de données, l'affichage via la méthode toString
  • Elle ne permet pas à un utilisateur de manipuler les données via des formulaires
  • Pour corriger ces lacunes, nous reprenons le design de LivreMetier qui sera éclatée en trois classes
    • une classe EntiteLivre.php qui ne traite que les informations des livres (numéro, titre, dépot légal)
    • une classe MyPDO qui permet de faire les requêtes élémentaires SQL sur une table quelconque
    • une classe VueLivre qui offre l'outillage nécessaire pour visualiser un livre ou l'éditer
    Schéma UML de la classe EntiteLivre Schéma UML de la classe VueLivre

Classe EntiteLivre

Class MyPDO

Class VueLivre

Appli CRUD

  • index.php
  • action.php

Résultat :
screenshot

Appli CRUD (v2)

  • Amélioration des Entités : diagramme UML
    • Classe AbstractEntite pour gérer l'attribut $persistant
    • Classe EntiteLivre qui intègre des informations de la structure de la table dans la base de données : noms et type des colonnes, clé primaire, auto_increment, clés étrangères, ...
      class EntiteLivre extends AbstractEntite
      {
          const TABLENAME = 'livre';
          static $COLNAMES = array('liv_num', 'liv_titre', 'liv_depotlegal');
          static $COLTYPES = array('number', 'text', 'date'); // par facilité, les types des formulaires => à améliorer
          static $PK = array('liv_num');  // tableau pour une éventuelle clé composite
          static $AUTOID = FALSE; // booléen indiquant si le renseignement de la clé est automatisé
          static $FK = array();  // tableau pour les éventuelles clés étrangères
      
          protected $liv_num;
          protected $liv_titre;
          protected $liv_depotlegal;
      
                      // pas dechangement pour la suite ../..
  • Automate à mettre en oeuvre : automate des états/actions du contrôleur CRUD
  • Contrôleur :
    
    // initialisation des variables $contenu et $message pour alimenter <body>
    $contenu = "";
    $message = "";
    // initialisation du connecteur myPDO pour la connexion
            // (sans nom de Table à renseigner selon le contexte)
    $myPDO = new MyPDO('mysql', $_ENV['host'], $_ENV['db'], $_ENV['user'], $_ENV['password']);
    
    if (!isset($_GET['action']))
       $_GET['action'] = "initialiser";
    
    switch ($_GET['action']) {
       case 'initialiser':
           $_SESSION['état'] = 'Accueil';
           break;
       case 'selectionnerTable':
           $myPDO->setNomTable($_GET['table_name']);
           $_SESSION['état'] = 'afficheTable';
           $_SESSION['table_name'] = $_GET['table_name'];
           break;
       case 'supprimerEntité':
          $myPDO->setNomTable($_SESSION['table_name']);
          // récupération du nom de colonne dans le GET
          $keyName = array_keys(array_diff_key($_GET, array('action'=>TRUE)))[0];
          $myPDO->delete(array($keyName => $_GET[$keyName]));
          $message .= "<p>Entité ". $_GET[$keyName]." supprimée</p>\n";
          $_SESSION['etat'] = 'afficheTable';
          break;
       case 'créerEntité': // construction du formulaire de création de l'entité
          $myPDO->setNomTable($_SESSION['table_name']);
    
          // Réflection pour récupérer la structure de l'entité
          $classeEntite = new ReflectionClass("lmsf\Entite".ucfirst($_SESSION['table_name']));
          $colNames = $classeEntite->getStaticPropertyValue("COLNAMES");
          $colTypes = $classeEntite->getStaticPropertyValue("COLTYPES");
          $paramForm = array_combine($colNames,$colTypes);
          if ($classeEntite->getStaticPropertyValue("AUTOID"))
             $paramForm = array_diff_key($paramForm,
                              array($classeEntite->getStaticPropertyValue(("PK"))[0] => TRUE));
          // $paramForm est un tableau associatif destiné à configurer le formulaire
          var_dump($paramForm);
    
          // Réflection pour récupérer la bonne vue
          $classeVue = new ReflectionClass("lmsf\Vue" . ucfirst($_SESSION['table_name']));
          $vue = $classeVue->newInstance();
          $contenu .= $vue->getForm4Entity($paramForm, "insérerEntité");
    
          // valeur par défaut non géré ci-dessus
          //$contenu .= $vue->getForm4Entity(array('liv_num' => array('type' => 'number',
          //                                       'default' => $nbEntites + 1),
          //                                       'liv_titre' => 'text',
          //                                       'liv_depotlegal' => 'date'), "insérerEntité");
    
          $_SESSION['état'] = 'formulaireTable';
          break;
       case 'modifierEntité': // construction du formulaire de modification de l'entité
          // ../..
          $_SESSION['état'] = 'formulaireTable';
          break;
       case 'insérerEntité':  // validation du formulaire de création d'une entité
          $myPDO->setNomTable($_SESSION['table_name']);
    
          // Réflection pour récupérer la structure de l'entité
          $classeEntite = new ReflectionClass("lmsf\Entite".ucfirst($_SESSION['table_name']));
          $colNames = $classeEntite->getStaticPropertyValue("COLNAMES");
          $colTypes = $classeEntite->getStaticPropertyValue("COLTYPES");
    
          $paramInsert = array_diff_key($_GET, array('action'=>'insérerEntité'));
          if ($classeEntite->getStaticPropertyValue("AUTOID"))
            $paramInsert = array_merge([$classeEntite->getStaticPropertyValue(("PK"))[0] => null],
                                        $paramInsert);
          var_dump($paramInsert);
    
          $myPDO->insert($paramInsert);
          // avant :  $myPDO->insert(['liv_num' => $_GET['liv_num'],
          //                          'liv_titre' => $_GET['liv_titre'],
          //                          'liv_depotlegal' => $_GET['liv_depotlegal'] ] );
    
          $entite = "?"; //$myPDO->get('liv_num',$_GET['liv_num']);
          $message .= "<p>Entité $entite crée</p>\n";
          $_SESSION['état'] = 'afficheTable';
          break;
       case 'sauverEntité':  // validation  du formulaire de modification de l'entité
          // ../..
    
          $_SESSION['état'] = 'afficheTable';
          break;
       default:
          $message .= "<p>Action " . $_GET['action'] . " non implémentée.</p>\n";
          $_SESSION['etat'] = 'Accueil';
    }
    
    switch ($_SESSION['état']) {
       case 'Accueil':
          $contenu .= getListeTables();
          break;
       case 'afficheTable' :
          $classeVue = new ReflectionClass("lmsf\Vue".ucfirst($_SESSION['table_name']));
          $vue = $classeVue->newInstance();
          $lesEntites = $myPDO->getAll();
          $contenu .= $vue->getAllEntities($lesEntites);
          break;
       case 'formulaireTable':
          //rien à faire, tout est fait dans la gestion des Actions ?
          break;
       default:
          $message .= "<p>état ".$_SESSION['etat']." inconnu</p>\n";
          $_SESSION['etat'] = 'Accueil';
    }
    
    
    // ajout d'un lien vers la page d'accueil
    $contenu .= "<p><a href='index.php?action=initialiser'>Accueil</a></p>\n";
    
    // ../..
  • Démo (visible uniquement lors du CM, screencast sur eureka)

  • Améliorations : gestion des valeurs obligatoires/non obligatoires dans les formulaires, ...
  • Gestion des clés étrangères, production d'une méthode de Vue pour les listes de sélections, ...

REST (representational state transfer)

  • REST : style d'architecture logicielle définissant un ensemble de contraintes à utiliser pour créer des services web.
  • Les services web conformes au style d'architecture REST sont appelés services web RESTful
  • Chaque URI (I pour Identifier) est une représentation textuelle d'une collection d'éléments ou d'un élément qui peuvent être manipulés à l'aide d'opérations prédéfinies et sans état.
  • Nécessite en général de mettre en place de la récriture d'URI.
    • http://www.site.net/livres indique une collection (livres au pluriel)
    • http://www.site.net/livre/100/ indique un élément précis (livre au singulier)
  • Les opérations possibles sur les collections ou les éléments sont associées aux verbes HTTP ainsi :
    Verbe HTTP GET PUT POST DELETE
    URI de collection (ex : http://www.site.net/livres/ ) Récupère les URI et éventuellement d'autres détails des éléments de la collection. Remplace la collection complète. Crée un nouvel élément dans la collection, l'URI du nouvel élément est attribué automatiquement. Supprime la collection
    URI d'élément (ex : http://www.site.net/livre/100/ ) Récupère une représentation de l'élément sélectionné de la collection, exprimée dans un type de médias approprié. Remplace l'élément sélectionné de la collection, ou s'il n'existe pas, le crée. rarement utilisé Supprime l'élément sélectionné de la collection
  • La méthode GET est sûre (en anglais, on peut dire nullipotent), c'est-à-dire qu'elle n'a pas d'effet de bord : ni la recherche ni l'accès ne modifient l'enregistrement.
  • Les méthodes PUT et DELETE sont idempotentes, c'est-à-dire que la répétition d'une même requête ne change pas l'état du système.

Namespaces

  • Les espaces de nommage PHP sont destinés à éviter les collisions de nommage entre code interne, externe, tiers,...
  • Ils offrent aussi la possibilité de raccourcir certains noms à rallonge
  • Usuellement les namespaces correspondent à des répertoires (même principe que pour les packages Java)
  • Déclaration d'un namespace (avant tout autre chose dans un fichier)
    <?php
        namespace coursPHP;
    
        const NBDIAPOS=200;
        class coursPHP { /* ... */ }
    ?>
  • Hiérarchisation des namespaces
    <?php
        namespace coursPHP\Lic2;
    
        const NBDIAPOS=123;
        class coursPHP { /* ... */ }
    
    on distingue ainsi les constantes coursPHP\NBDIAPOS et coursPHP\Lic2\NBDIAPOS, les classes coursPHP\coursPHP et coursPHP\Lic2\coursPHP définies dans les 2 namespaces.
  • Mécanisme également valable pour les fonctions

Classes et Namespaces

  • Une classe peut être définie au sein d'un namespace :
    <?php
    namespace coursPHP;
    
    class ClasseExemplePHP
    {
        public function method(): string {
            return "Methode de ClasseExemplePHP situé dans le namespace : ". __NAMESPACE__;
        }
    }
    ?>
  • Usage :
    require "ClasseExemplePHP.php";
    
    use coursPHP\ClasseExemplePHP;
    
    $objet = new ClasseExemplePHP;
    echo $objet->method();
  • Exécution :
  • La constante magique __NAMESPACE__ contient le nom du namespace courant.
  • L'instruction use permet également d'établir des alias de classe :
    use coursPHP\ClasseExemple as ClassExemple; // alias par défaut
  • use ne dispense pas de charger en mémoire le code source des classes avec require ou include.
  • Il est possible d'organiser hiérarchiquement les namespaces sous forme d'arborescence, dans ce cas l'identification d'un namespace pourra se faire de façon relative ou absolue comme dans un système de fichier.

Autochargement des classes

  • En PHP, les débuts de fichiers tournent parfois à l'accumulation de require...
  • C'est aussi fastidieux que source d'oublis ou d'erreurs
  • Via la SPL (encore elle !), PHP offre un système efficace pour charger automatiquement les classes nécessaires à nos programmes : la fonction spl_autoload_register()
  • spl_autoload_register() enregistre une fonction pour implémenter __autoload()
  • Signature :
    spl_autoload_register ( callable $autoload_function = ? , bool $throw = true , bool $prepend = false ) : bool
  • Exemple de fonction __autoload :
    function monChargeurDeClasse_V1(string $className) {
        include './'.$className.'.php';
    }
    spl_autoload_register('monChargeurDeClasse_V1'); 
  • Exemple similaire avec une fonction anonyme :
    spl_autoload_register(function (string $className) {
                            include './'.$className.'.php';
                            }
                        ); 
  • Traitement des namespaces : exemple avec la classe coursPHP\ClassExemplePHP
    • Si le namespace correspond à un dossier il faut remplacer le backslash (\) par un slash (/) et toujours rajouter l'extension :
      spl_autoload_register(function (string $className) {
                              // il faut échapper le backslash d'où le '\\'
                              $className = str_replace('\\','/',$className);
                              include './'.$className.'.php';
                              }
                          ); 
    • Si l'autochargement se fait depuis un namespace, il faut l'enlever du chemin relatif :
      spl_autoload_register(function (string $className) {
                              $className = str_replace(__NAMESPACE__.'\\','',$className);
                              $className = str_replace('\\','/',$className);
                              include './'.$className.'.php';
                              }
                          ); 
  • Quoi qu'il arrive : toujours adapter à son architecture (dossier class, dossiers correspondants aux namespaces, ...) et ses conventions de nommage (.php, .class.php, ...)
  • Contrôle d'accès à une ressource

    Histoire des Serveurs Web

    Apache HTTP Server

    Installation Apache HTTP Server sous Ubuntu

    Configuration Apache HTTP Server

    Association Apache-PHP

    URL vs Système de fichiers

    Les cookies

    Usages courants

    Où sont-ils stockés ?

    Format d'un cookie

    Cookies et HTTP

    Cookies et PHP

    The Model-View-Controllor design pattern in PHP

    Prerequisite

    A few needed technologies

    • HTTP
    • REST
    • Security

    Hypertext Transfer Protocol

    Application-level protocol for distributed systems. Generic and stateless.

    • Request message (from client to server)
    • Response message (from server to client)
    • Text-based messages / Multipurpose Internet Mail Extensions (MIME) format
    • Messages
      generic-message = start-line
                      *(message-header CRLF)
                      CRLF
                      [ message-body ]
      start-line      = Request-Line | Status-Line

    HTTP Headers and Entities

    message-header = field-name ":" [ field-value ]
    field-name     = token
    field-value    = *( field-content | LWS )
    field-content  = <the OCTETs making up the field-value
                     and consisting of either *TEXT or combinations
                     of token, separators, and quoted-string>

    Example request headers and entities

    Host: tools.ietf.org
    Connection: keep-alive
    Cache-Control: max-age=0
    Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
    Accept-Encoding: gzip,deflate,sdch
    Accept-Language: en-US,en;q=0.8
    Accept-Charset: UTF-8,*;q=0.5
    User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) AppleWebKit/537.17 (KHTML, like Gecko) Chrome/24.0.1312.57 Safari/537.17
    

    HTTP Requests

    From the client to the server

    Request = Request-Line
              \*(( general-header
                | request-header
                | entity-header ) CRLF)
              CRLF
              [ message-body ] 

    Request Line

    Request-Line = Method SP Request-URI SP HTTP-Version CRLF
    

    HTTP Methods

    • "OPTIONS": information about the communication options available
    • "GET": retrieve whatever information is identified by the Request-URI
    • "HEAD": same as "GET" message-body in the response
    • "POST": append entity to the existing Request-URI
    • "PUT": store entity as the new Request-URI
    • "DELETE": delete existing Request-URI
    • "TRACE" : see what is being received at the other end of the request chain
    • "CONNECT" : for use with a proxy that can dynamically switch to being a tunnel (e.g. SSL tunneling).

    HTTP Responses

    Response    = Status-Line
                  *(( general-header
                   | response-header
                   | entity-header ) CRLF)
                  CRLF
                  [ message-body ]
    Status-Line = HTTP-Version SP Status-Code SP Reason-Phrase CRLF
    

    Status Codes

    • 1xx: Informational - Request received, continuing process
    • 2xx: Success - The action was received, understood, and accepted
    • 3xx: Redirection - Further action must be taken to complete the request
    • 4xx: Client Error - The request contains bad syntax or cannot be fulfilled
    • 5xx: Server Error - The server failed to fulfill an apparently valid request

    Representational State Transfer

    REST is a style of software architecture for distributed systems on top of HTTP.

    • Each resource are accessed through one unique request (URI).
    • Requests are stateless (identification within the URI)
    • Resources are accessed one by one or as collections
    • RESTful web service (RESTful web API)

    RESTful Web Services

    RESTful API and HTTP methods

    Resource GET PUT POST DELETE
    Collection URI List elements Replace entire collection Create new element in collection Delete collection
    Element URI Retrieve one element Replace existing element *Generally not used* Delete one element
    • Plural may indicate a collection e.g. http://example.com/emails/
    • An id may indicate an element e.g. http://example.com/email/17/
    • URN can be prefixed with API version e.g. https://api.twitter.com/1.1/statuses/home_timeline.json

    Security

    Too many vulnerabilities exist... But developers are responsible for their code!

    Common Attacks

    • Cross-Site Request Forgery (CSRF)
    • Cross-site scripting (XSS)
    • SQL injection

    Common Measures

    • Anti-CSRF tokens
    • Forms Timeouts
    • Escape users inputs

    Reduce vulnerability... Use frameworks!

    MVC

    The Model–View–Controller Design Pattern

    • Improve the separation of concerns
    • Facilitate automated unit testing
    • Facilitate team work

    Model

    • Holds the data
    • Links to persistent storage (DBMS)
    • Ignores other components

    View

    • Representation of data
    • What users see
    • May know the Model

    Controller

    • Handles users requests
    • Updates Model data
    • Triggers Views

    MVC Schema

    MVC overview

    MVC Sequence

    MVC Sequence

    A simple PHP script

    Show news and allow comments on them.

    <?php
    $connect = mysql_connect('myserver', 'mylogin', 'mypassword');
    mysql_select_db('myDB');
    if ($_SERVER['REQUEST_METHOD'] == 'POST') {
        $news_id = $_POST['news_id'];
        mysql_query("INSERT INTO commentaires SET news_id='$news_id',
          auteur='".mysql_escape_string($_POST['auteur'])."',
          texte='".mysql_escape_string($_POST['texte'])."',
          date=NOW()"
          );
        header("location: ".$_SERVER['SCRIPT_NAME']."?news_id=$news_id");
        exit;
    } else {
        $news_id = $_GET['news_id'];
    }
    ?>
    <!-- [...] -->
    
    <!-- [...] -->
    <html><head><title>Les news</title></head>
    <body>
    <h1>Les news</h1>
    <div id="news">
      <?php
      $news_req = mysql_query("SELECT * FROM news WHERE id='$news_id'");
      $news = mysql_fetch_array($news_req);
      ?>
      <h2><?php echo $news['titre'] ?> postée le <?php echo $news['date'] ?></h2>
      <p><?php echo $news['texte_nouvelle'] ?> </p>
      <?php
      $comment_req = mysql_query("SELECT * FROM commentaires
          WHERE news_id='$news_id'");
      $nbre_comment = mysql_num_rows($comment_req);
      ?>
      <h3><?php echo $nbre_comment ?> commentaires relatifs à cette nouvelle</h3>
      <?php while ($comment = mysql_fetch_array($comment_req)) {?>
      <h3><?php echo $comment['auteur'] ?>
          a écrit le <?php echo $comment['date'] ?></h3>
      <p><?php echo $comment['texte'] ?></p>
      <?php } ?>
    <!-- [...] -->
    
    <!-- [...] -->
      <form method="POST" action="<?php echo $_SERVER['SCRIPT_NAME'] ?>"
          name="ajoutcomment">
          <input type="hidden" name="news_id" value="<?php echo $news_id?>">
          <input type="text" name="auteur" value="Votre nom"><br />
          <textarea name="texte" rows="5" cols="10">
              Saisissez votre commentaire
          </textarea><br />
          <input type="submit" name="submit" value="Envoyer">
      </form>
    </div>
    </body>
    </html>
    

    Various actions are mixed up in this file:

    • Request handling
    • Database update
    • Database lookup
    • Visualization (style?)
    • Security
    • Routing
    ## The Simplest MVC App Same as previous example, with MVC pattern ### The Model
    <?php
    function dbconnect() {
      static $connect = null;
      if ($connect === null) {
        try {
          $connect = new PDO("mysql:dbname=simplemvc;host=127.0.0.1", 'pigne', 'n2EfCJYFx6CExzSX' );
          $connect->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
        } catch (PDOException $e) { echo 'Connection failed :( : ' . $e->getMessage(); exit;}
      }
      return $connect;
    }
    /* [...] */
    
    /* [...] */
    function get_news($id) {
      try{
        $sql = "SELECT * FROM news WHERE id= :id";
        $sth = dbconnect()->prepare($sql);
        $sth->execute(array(':id' => $id));
        if($sth->errorCode() == 0) {
          return  $sth->fetch();
        }
        else {
          return array();
        }
      }catch (PDOException $e) { echo 'Select comments failed: ' . $e->getMessage(); exit;}
    }
    
    function get_comments($news_id) {
      try {
        $sql = "SELECT * FROM commentaires WHERE news_id= :news_id";
        $sth = dbconnect()->prepare($sql);
        $sth->execute(array(':news_id' => $news_id));
        if($sth->errorCode() == 0) {
          return  $sth->fetchAll();
        }
        else {
          return array();
        }
      }catch (PDOException $e) { echo 'Select comments failed: ' . $e->getMessage(); exit;}
    }
    /* [...] */
    
    /* [...] */
    function insert_comment($comment) {
      $connect = dbconnect();
      try{
        $sql =  "INSERT INTO commentaires SET news_id= :news_id , " .
        "auteur= :auteur , " .
        "texte= :texte , " .
        "date=NOW()";
        $sth = $connect->prepare($sql);
        $sth->execute(array(':news_id' => (int)$comment['news_id'],
          ':auteur' => $connect->quote($comment['auteur']),
          ':texte' => $connect->quote($comment['texte']),
          )
        );
      } catch(PDOException $e) { echo 'Insert failed: ' . $e->getMessage(); exit;}
    }
    

    The View

    <html><head><title>Les news</title></head>
    <body>
      <h1>Les News</h1>
      <div id="news">
        <h2><?php echo $news['titre'] ?> postée le <?php echo $news['date'] ?></h2>
        <p><?php echo $news['texte_nouvelle'] ?> </p>
        <h3><?php echo $nbre_comment ?> commentaires relatifs à cette nouvelle</h3>
        <dl>
        <?php foreach ($comments AS $comment) {?>
        <dt><?php echo $comment['auteur'] ?>, le <?php echo $comment['date']?>:</dt>
        <dd><?php echo $comment['texte'] ?></dd>
        <?php } ?>
        </dl>
        <h3>Un commentaire ?</h3>
        <form method="POST" action="<?php echo $_SERVER['SCRIPT_NAME'] ?>" name="ajoutcomment">
          <input type="hidden" name="news_id" value="<?php echo $news['id']?>">
          <input type="text" name="auteur" placeholder="Votre nom"><br>
          <textarea name="texte" placeholder="Saisissez votre commentaire"></textarea><br>
          <input type="submit" name="submit" value="Envoyer">
        </form>
      </div>
    </body></html>
    

    The Controller

    require ('simpleModel.php');
    if ($_SERVER['REQUEST_METHOD'] == 'POST') {
      insert_comment($_POST);
      header("HTTP/1.1 301 Moved Permanently");
      header("location: {$_SERVER['SCRIPT_NAME']}?news_id={$_POST['news_id']}");
      exit;
    } else {
      $news = get_news($_GET['news_id']);
      $comments = get_comments($_GET['news_id']);
      $nbre_comment = sizeof($comments);
      require ('simpleView.php');
    }
    

    PHP MVC Frameworks

    Many of them

    • Agavi
    • CakePHP
    • CodeIgniter
    • Copix
    • Dynatrix
    • FuelPHP
    • Gest-HVSL
    • Hoa
    • Jelix
    • Kinkama
    • Kohana
    • MODx
    • Open Web Framework
    • Postnuke
    • QCodo
    • Symfony
    • TemplatePP
    • Yii Framework
    • Zend Framework
    • Joomla! Platform

    Symfony framework

    • Framework MVC libre écrit en PHP
    • Produit par SensioLabs (cocorico)
    • Mise en oeuvre du pattern MVC : séparation du code en 3 couches
    • Mapping Objet Relationnel intégré (Doctrine)
    • Gestion d'URL parlantes (Routes)
    • ...
    • Site officiel
    • Les versions de Symfony

    Architecture d'un projet Symfony

    Les routes

    • Quelles URL préférez-vous ?
      • https://www.univ-lehavre.fr/spip.php?rubrique71
      • https://www.univ-lehavre.fr/formations
    • Un des fondements du web selon TBL était justement de disposer d'un système d'adressage simple et compréhensible...
    • Symfony permet de concevoir des URLs explicites via la notion de route
    • Les controleurs définis dans symfony permettent d'associer une route à une action
    • Les routes sont paramétrables et configurables à volonté

    Les controleurs (SF 3.4)

    • Dans le répertoire src/AppBundle/Controller/ on peut ajouter des controleurs gérant des routes associées à des actions.
    • Ainsi la classe HelloWorldController permet de créer un programme Hello World ! dans Symfony :
      namespace AppBundle\Controller;
      
      use Symfony\Component\HttpFoundation\Response;
      use Symfony\Component\Routing\Annotation\Route;
      
      class HelloWorldController
      {
      
          /**
           * @Route("/HelloWorld")
           */
          public function helloWorldAction()
          {
               return new Response(
                  '<html><body><h1>Hello World !</h1></body></html>'
              );
          }
      
      }
    • La directive @Route définit une URL
    • La méthode d'action associée est un controleur qui renvoie une instance de Response.
    • Symfony offre un système de templates permettant de produire plus efficacement des pages HTML : twig
    • classe HelloWorldController (v2)
      namespace AppBundle\Controller;
      
      use Symfony\Component\HttpFoundation\Response;
      use Symfony\Component\Routing\Annotation\Route;
      use Symfony\Bundle\FrameworkBundle\Controller\Controller;
      
      class HelloWorldController extends Controller
      {
      
          /**
           * @Route("/HelloWorld")
           */
          public function HelloWorldAction()
          {
               return new Response(
                  '<html><body><h1>Hello World !</h1></body></html>'
              );
          }
      
          /**
          * @Route("HelloWorld/mister")
           */
          public function HelloMisterAction()
          {
               $mister = "Fournier";
               return $this->render('HelloWorld/mister.html.twig', array('lastName' => $mister));
          }
      
      }
      
    • Le fichier app/Resources/views/HelloWorld/mister.html.twig :
      {# app/Resources/views/HelloWorld/mister.html.twig #}
      
                  <h1>Hello Mister {{ lastName }} !</h1>

    Les routes paramétrables et nommables

    À propos de twig

    • Récapitulatif 
      • {# commentaires ici #}
      • {{ variable ici }}
      • {% structure de contrôle ici %}
    • Template et héritage
      • le tag {% extends %} permet d'hériter d'un template de base
      • les tags {% block bloc_name %} et {% endblock %} définissent un bloc surchargeable
      • Au sein d'un bloc {{ parent() }} permet de récupérer le contenu du bloc issu du template parent
      • Fichier baseHello.html.twig :
        <!DOCTYPE html>
        <html>
        <head>
            <meta charset="UTF-8" />
            <title>{% block title %}Welcome!{% endblock %}</title>
            {% block stylesheets %}{% endblock %}
            <link rel="icon" type="image/x-icon" href="{{ asset('logoULH.ico') }}" />
        </head>
        <body>
        <h1>{% block h1 %}{% endblock %}</h1>
        {% block article %}
        <p>Ce texte est hérité du template parent !</p>
        {% endblock %}
        
        {% block javascripts %}{% endblock %}
        </body>
        </html>
        
      • Fichier title.html.twig :
        {# app/Resources/views/HelloWorld/title.html.twig #}
        {%  extends 'HelloWorld/baseHello.html.twig' %}
        
        {% block title %}Hello {{ title }} {{ lastName }} ! {%  endblock %}
        {% block h1 %}Hello {{ title }} {{ lastName }} ! {%  endblock %}
        {% block article %}
            {{ parent()  }}
            <p>Et j'y ajoute un complément...</p>
        {% endblock %}

    Symfony 4.x : ce qui change

    • Architecture d'un projet Symfony :
      • config/ pour configurer l'environnement (routes, services, packages)
      • src/ pour le code PHP
      • templates/ pour les twig
      • bin/ les executables nécessaires dont bin/console qui permet notamment de génerer un environnement d'exécution complet
      • var/ pour les fichiers générés automatiquement : logs, sessions , cache
      • vendor/ bibliothèques tiers
      • public/ tout ce qui doit être accessible : css, js, images...
    • Le controleur de base n'est plus défini dans la classe :
      Symfony\Bundle\FrameworkBundle\Controller\Controller mais dans la classe Symfony\Bundle\FrameworkBundle\Controller\AbstractController, il faut en faire hériter nos controleurs pour hériter de ses fonctionnalités, exemple :
      namespace App\Controller;
      
      use Symfony\Component\HttpFoundation\Response;
      use Symfony\Component\Routing\Annotation\Route;
      use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
      
      class HelloWorldController extends AbstractController
      {
      
          /**
           * @Route("/HelloWorld")
           */
          public function HelloWorldAction()
          {
              return new Response(
                  '<html><body><h1>Hello World !</h1></body></html>'
              );
          }
      
      } 
    • ...

    ORM

    • ORM : Object Relational Mapper
    • Symfony utilise Doctrine
    • Les entités sont des classes qui correspondent aux tables des BD dans lesquelles :
      • les attributs d'instances sont associées aux colonnes,
      • des annotations (commentaires associés au code) décrivent les liens entre classe et table ou entre attributs et colonnes
      • les accesseurs et modifieurs sont définis.
    • Configuration de la base de données :
      • Fichier app/config/parameters.yml
        parameters:
            database_host: ust-infoserv.univlehavre.lan
            database_port: null
            database_name: xy123456
            database_user: xy123456
            database_password: *****
                            (...)
    • Création d'une entité en ligne de commande :
      $ php bin/console doctrine:generate:entity
      
        Welcome to the Doctrine2 entity generator
      
      This command helps you generate Doctrine2 entities.
      
      First, you need to give the entity name you want to generate.
      You must use the shortcut notation like AcmeBlogBundle:Post.
      
      The Entity shortcut name: AppBundle:Collectivite
      
      Determine the format to use for the mapping information.
      
      Configuration format (yml, xml, php, or annotation) [annotation]:
      
      Instead of starting with a blank entity, you can add some fields now.
          Note that the primary key will be added automatically (named id).
      
      Available types: array, simple_array, json_array, object,
      boolean, integer, smallint, bigint, string, text, datetime, datetimetz,
      date, time, decimal, float, binary, blob, guid.
      
      New field name (press <return> to stop adding fields): col_code
      Field type [string]: string
      Field length [255]: 3
      Is nullable [false]:
      Unique [false]: true
      
      New field name (press <return> to stop adding fields): col_nom
      Field type [string]:
      Field length [255]: 50
      Is nullable [false]:
      Unique [false]:
      
      New field name (press <return> to stop adding fields): col_population
      Field type [string]: integer
      Is nullable [false]:
      Unique [false]:
      
      New field name (press <return> to stop adding fields): col_superficie
      Field type [string]: integer
      Is nullable [false]:
      Unique [false]:
      
      New field name (press <return> to stop adding fields): col_region
      Field type [string]:
      Field length [255]: 30
      Is nullable [false]:
      Unique [false]:
      
      New field name (press <return> to stop adding fields):
      
        Entity generation
      
        created ./src/AppBundle/Entity/Collectivite.php
      > Generating entity class src/AppBundle/Entity/Collectivite.php: OK!
      > Generating repository class src/AppBundle/Repository/CollectiviteRepository.php: OK!
      
      
        Everything is OK! Now get to work :).
      
    • Classe AppBundle/Entity/Collectivite.php :
      namespace AppBundle\Entity;
      
      use Doctrine\ORM\Mapping as ORM;
      
      /**
        * Collectivite
        *
        * @ORM\Table(name="collectivite")
        * @ORM\Entity(repositoryClass="AppBundle\Repository\CollectiviteRepository")
        */
      class Collectivite
      {
          /**
           * @var int
           *
           * @ORM\Column(name="id", type="integer")
           * @ORM\Id
           * @ORM\GeneratedValue(strategy="AUTO")
              */
          private $id;
      
          /**
           * @var string
           *
           * @ORM\Column(name="col_code", type="string", length=3, unique=true)
           */
          private $colCode;
      
          /**
           * @var string
           *
           * @ORM\Column(name="col_nom", type="string", length=50)
           */
          private $colNom;
      
          /**
           * @var int
           *
           * @ORM\Column(name="col_population", type="integer")
           */
          private $colPopulation;
      
          /**
           * @var int
           *
           * @ORM\Column(name="col_superficie", type="integer")
           */
          private $colSuperficie;
      
          /**
           * @var string
           *
           * @ORM\Column(name="col_region", type="string", length=30)
           */
          private $colRegion;
      
      
          /**
           * Get id
           *
           * @return int
           */
          public function getId()
          {
              return $this->id;
          }
      
                  /* (...) suite des getters et des setters */
      
      
    • validation / maj des schemas
      $ php bin/console doctrine:schema:validate
      $ php bin/console doctrine:schema:update [ --force | --dump-sql ]
      
      Table produite sur le SGBD :
      mysql> describe collectivite;
      +----------------+-------------+------+-----+---------+----------------+
      | Field          | Type        | Null | Key | Default | Extra          |
      +----------------+-------------+------+-----+---------+----------------+
      | id             | int(11)     | NO   | PRI | NULL    | auto_increment |
      | col_code       | varchar(3)  | NO   | UNI | NULL    |                |
      | col_nom        | varchar(50) | NO   |     | NULL    |                |
      | col_population | int(11)     | NO   |     | NULL    |                |
      | col_superficie | int(11)     | NO   |     | NULL    |                |
      | col_region     | varchar(30) | NO   |     | NULL    |                |
      +----------------+-------------+------+-----+---------+----------------+
      6 rows in set (0,00 sec)
      
    • Rien n'empêche de construire soit même ses entités.
    • Les annotations en détail
    • Controleur et gestion des actions CRUD
      • Exemple sur l'entité LivreSF mappé sur une table LivreSF
      • mysql> describe livresf;
        +-------------+--------------+------+-----+---------+----------------+
        | Field       | Type         | Null | Key | Default | Extra          |
        +-------------+--------------+------+-----+---------+----------------+
        | liv_num     | int(11)      | NO   | PRI | NULL    | auto_increment |
        | liv_titre   | varchar(100) | NO   |     | NULL    |                |
        | auteursf_id | int(11)      | YES  | MUL | NULL    |                |
        +-------------+--------------+------+-----+---------+----------------+
        3 rows in set (0,00 sec)
        
        
      • Controlleur LivreSFController
        namespace AppBundle\Controller;
        
        use AppBundle\Entity\LivreSF;
        use Symfony\Component\HttpFoundation\Response;
        use Symfony\Component\Routing\Annotation\Route;
        
        class LivreSFController extends DefaultController { 
      • Création d'une entité
            /**
             * @Route("livresf/create")
             */*/
            public function createAction() {
                // you can fetch the EntityManager via $this->getDoctrine()
                // or you can add an argument to your action: createAction(EntityManagerInterface $em)
                $entityManager = $this->getDoctrine()->getManager();
        
                $livresf = new LivreSF();
                $livresf->setLivTitre('Le cerveau solitaire');
        
                // tells Doctrine you want to (eventually) save the Product (no queries yet)
                $entityManager->persist($livresf);
        
                // actually executes the queries (i.e. the INSERT query)
                $entityManager->flush();
        
                return new Response('Saved new livresf with liv_num '.$livresf->getLivNum());
            }
        
      • Accès à une entité
             /**
             * @Route("livresf/{livresfId}")
             */
            public function showAction($livresfId)
            {
                $livresf = $this->getDoctrine()
                    ->getRepository(LivreSF::class)
                    ->find($livresfId);
        
                if (!$livresf) {
                    throw $this->createNotFoundException(
                        'No livresf found for id '.$livresfId
                    );
                }
                else {
                    return $this->render('livresf.html.twig', array(
                        'liv_num' => $livresf->getLivNum(), 'liv_titre' => $livresf->getLivTitre(),
                         ));
                }
            }
        
      • Accès à toutes les entités
             /**
             * @Route("livresf", name = "accueil")
             */
            public function showAllAction()
            {
                $lesLivresf = $this->getDoctrine()
                    ->getRepository(LivreSF::class)
                    ->findAll();
        
                return $this->render('leslivresf.html.twig', array(
                    'leslivressf' => $lesLivresf
                    ));
        
            }
      • Mise à jour d'une entité
            /**
             * @Route("livresf/update/{livresfId}")
             */
            public function updateAction($livresfId)
            {
                $entityManager = $this->getDoctrine()->getManager();
                $livresf = $entityManager->getRepository(LivreSF::class)->find($livresfId);
        
                if (!$livresf) {
                    throw $this->createNotFoundException(
                        'No livresf found for id '.$livresfId
                    );
                }
                $titres = array('Le cerveau solitaire', 'Le voyageur de l\'inconnu', 'Les mutants', 'La galaxie noire');
                if ($livresfId <= sizeof($titres))
                    $livresf->setLivTitre($titres[$livresfId - 1]);
                else
                    $livresf->setLivTitre('titre inconnu');
                $entityManager->flush();
        
                return $this->redirectToRoute('accueil');
            }
        
      • Suppression d'une entité
            /**
             * @Route("livresf/delete/{livresfId}")
             */
            public function deleteAction($livresfId)
            {
                $entityManager = $this->getDoctrine()->getManager();
                $livresf = $entityManager->getRepository(LivreSF::class)->find($livresfId);
        
                if (!$livresf) {
                    throw $this->createNotFoundException(
                        'No livresf found for id '.$livresfId
                    );
                    }
                else {
                    $entityManager->remove($livresf);
                    $entityManager->flush();
                }
                return $this->redirectToRoute('accueil');
            }
        
      • Filtre sur une entité
             /**
             * @Route("livresf/select/{livresfTitre}")
             */
            public function selectAction($livresfTitre)
            {
                $entityManager = $this->getDoctrine()->getManager();
                $livresf = $entityManager->getRepository(LivreSF::class)->findOneBy(array('liv_titre' => $livresfTitre));
        
                if (!$livresf) {
                    throw $this->createNotFoundException(
                        'No livresf found for titre '.$livresfTitre
                    );
                    }
                else {
                    return $this->render('livresf.html.twig', array(
                        'liv_num' => $livresf->getLivNum(), 'liv_titre' => $livresf->getLivTitre()
                    ));
                }
            }
        
        
      • Plein d'autres méthodes pour interroger la table : findOneBycolname(), findBycolname(), ...
        doctrine query language
    • Associations One To Many avec une table AuteurSF
      • Modification Entite LivreSF :
            /**
             * @ORM\ManyToOne(targetEntity="AuteurSF", inversedBy="livres")
             * @ORM\JoinColumn(name="auteursf_id", referencedColumnName="id")
             */
             private $auteur;
      • Entite AuteurSF :
        use Doctrine\Common\Collections\ArrayCollection;
        /**
         * @ORM\Entity
         * @ORM\Table(name="auteursf")
         */
        class AuteurSF
        {
            /**
             * @ORM\Column(type="integer")
             * @ORM\Id
             * @ORM\GeneratedValue(strategy="AUTO")
             */
            private $id;
        
        
            /**
             * @ORM\Column(type="string", length=100)
             */
            private $aut_nom;
        
            /**
             * @ORM\Column(type="string", length=100)
             */
            private $aut_prenom;
        
            /**
             * @ORM\OneToMany(targetEntity="LivreSF", mappedBy="auteur")
             */
            private $livres;
        
            public function __construct()
            {
                $this->livres = new ArrayCollection();
            }
                        
    • Demo

    Gestions de Formulaires

    • À l'image de Doctrine, Symfony met à disposition un environnement permettant de générer et traiter des formulaires.
    • Création de formulaires symfony
      • Les formulaires sont associés à des entités (comme Doctrine !)
      • Des classes décrivent les types d'input des formulaires :
        use Symfony\Component\Form\Extension\Core\Type\TextType;
        use Symfony\Component\Form\Extension\Core\Type\IntegerType;
        // ...
        Liste détaillée des types.
      • La classe FormBuilder permet de construire un formulaire :
            /**
             * @Route("livresf/form1/")
             */
            public function form1Action(Request $request) {
                /* Dans ce controller, on génère un formulaire permettant de saisir les informations d'une instance de livreSF. */
                $livresf = new LivreSF();
                $livresf->setLivTitre("Le cerveau solitaire");
                $livresf->setLivNum(90);
        
                $form = $this->createFormBuilder($livresf);
      • Il faut créer les inputs pour le numéro et le titre :
                $form->add('livtitre',TextType::class, array('attr' => array('maxlength' => 22)));
                $form->add('livnum', IntegerType::class);
      • Il faut générer le bouton d'envoi et préciser la méthode de transmission :
        
                $form->add('save', SubmitType::class, array('label' => 'Create livresf'));
                $form->setMethod("GET");
      • Le formulaire est prêt ! :
        
                $form->getForm();
      • Pour afficher le formulaire on passe par un template twig
        
                return $this->render("default/new.html.twig", array('form' => $form->createView()));
                }
      • Par défaut, l'attribut action du formulaire est associé à la route courante
      • Exemple complet (noter l'enchainement des méthodes de FormBuilder) :
            /**
             * @Route("livresf/form1/")
             */
            public function form1Action(Request $request) {
                $livresf = new LivreSF();
                $livresf->setLivTitre("Le cerveau solitaire");
                $livresf->setLivNum(90);
        
                $form = $this->createFormBuilder($livresf)
                    ->add('livtitre',TextType::class, array('attr' => array('maxlength' => 22)))
                    ->add('livnum', IntegerType::class)
                    ->add('save', SubmitType::class, array('label' => 'Create livresf'))
                    ->setMethod("GET")
                    ->getForm();
        
                return $this->render("default/new.html.twig", array('form' => $form->createView()));
            }
        
    • Affichage d'un formulaire avec twig :
      {# app/Resources/views/default/new.html.twig #}
      {{ form_start(form) }}
      {{ form_widget(form) }}
      {{ form_end(form) }}
      
    • Pour plus de possibilités avec les formulaires dans twig.
    • Gestion des attributs action et method des formulaires
      • La méthode par défaut est POST, donc dans ce cas pas besoin de setMethod("POST") !
            /**
             * @Route("livresf/form2/")
             */
            public function form2Action(Request $request) {
                $livresf = new LivreSF();
        
                $form = $this->createFormBuilder($livresf)
                    ->add('livtitre',TextType::class, array('attr' => array('maxlength' => 22)))
                    ->add('livnum', IntegerType::class)
                    ->add('save', SubmitType::class, array('label' => 'Create livresf'))
                    //->setMethod("POST") inutile car méthode par défaut
                    ->getForm();
        
                return $this->render("default/new.html.twig", array('form' => $form->createView()));
            }
        
      • Jusqu'ici les requêtes HTTP ne sont définies que via les routes et les réponses via la classe Response :
        use Symfony\Component\Routing\Annotation\Route;
        use Symfony\Component\HttpFoundation\Response;
        
      • La classe Request permet de configurer et réaliser une requête HTTP :
        use Symfony\Component\HttpFoundation\Request;
                        
      • Une instance de Request est passée en paramètre de l'action :
            public function form3Action(Request $request) {
      • Le controller récupère la gestion des données transmises par le formulaire :
                $form->handleRequest($request);
      • On précise comment gérer les données :
        
                if ($form->isSubmitted() && $form->isValid()) {
                    $livresf = $form->getData();
                            /* on fait ce qu'on veut de $livresf*/
      • On redirige si nécessaire l'utilisateur vers une route appropriée :
        
                    return $this->redirectToRoute('accueil');
                            }
      • Exemple complet :
            /**
             * @Route("livresf/form3/")
             */
            public function form3Action(Request $request) {
                $livresf = new LivreSF();
        
                $form = $this->createFormBuilder($livresf)
                    ->add('livtitre',TextType::class)
                    ->add('livnum', IntegerType::class)
                    ->add('save', SubmitType::class, array('label' => 'Create livresf'))
                    ->getForm();
        
                $form->handleRequest($request);
                if ($form->isSubmitted() && $form->isValid()) {
                    $livresf = $form->getData();
                    $entityManager = $this->getDoctrine()->getManager();
                    $entityManager->persist($livresf);
                    $entityManager->flush();
                    return $this->redirectToRoute('accueil');
                }
        
                return $this->render('default/new.html.twig', array(
                    'form' => $form->createView(),
                ));
            }
        
    • Validation de Formulaires
      • Le controller précédent permet de saisir une chaîne constituée d'espaces pour le titre, et dans ce cas :
        message d'erreur symfony
      • Des annotations dans le code des entités permettent de valider finement les informations issues des formulaires :
        /* (...) *.
        use Symfony\Component\Validator\Constraints as Assert;
        
        /* (...) *.
        class LivreSF
        {
            /**
             * @ORM\Column(type="string", length=100)
             * @Assert\NotBlank()
             */
            private $liv_titre;
        /* (...) *.
        
        Bloque le traitement du formulaire et place un message dans la page :
        message de validation symfony
      • Liste des contraintes annotables
      • On peut également créer des méthodes dans l'entité afin de mettre en oeuvre des validations complexes :
            /**
             * @Assert\IsTrue(message="Le titre du livre ne peut pas être 'Le Masque SF'")
             */
            public function isLivTitreOk()
            {
                return $this->liv_titre != "Le Masque SF";
            }
        

    Gestion des associations ManyToMany

    • Associations Many to Many voir doctrine
    • Dans ce nouvel exemple, les entités associées aux tables auteur et livre sont nommées Auteur et Livre.
    • La gestion des associations par Doctrine n'est pas symétrique : il y a une Entité qui est prioritaire et qui peut gérer les informations associatives : c'est l'entité dans laquelle les directives @JointTable sont présentes, ici l'entité Auteur et l'attribut livNumArray :
      /**
           * @var \Doctrine\Common\Collections\Collection
           *
           * @ORM\ManyToMany(targetEntity="Livre", inversedBy="aut")
           * @ORM\JoinTable(name="livaut",
           *   joinColumns={
           *     @ORM\JoinColumn(name="aut_id", referencedColumnName="aut_id")
           *   },
           *   inverseJoinColumns={
           *     @ORM\JoinColumn(name="liv_num", referencedColumnName="liv_num")
           *   }
           * )
           */
           private $livNumArray;
    • Modification de l'entité prioritaire ici Auteur::class avec une méthode permettant l'ajout d'un livre
          /**
           * @param Livre $livre
           */
          public function addLivre(Livre $livre) {
              $this->livNumArray->add($livre);
          }
    • Construction du formulaire v1 (liste de sélection)
      $livres = $this->getDoctrine()->getRepository(Livre::class)->findAll();
      
      $dataChoice = array();
      
      foreach ($livres as $livre) {
                  $livTitre = $livre->getLivTitre();
                  $dataChoice[$livTitre] = $livre->getLivNum();
      }
      
      $form = $this->createFormBuilder()
          ->add('liv_num', ChoiceType::class, array('choices' => $dataChoice))
          ->add('save', SubmitType::class, array('label' => 'Update livre'))
          ->getForm();
    • Construction du formulaire v2 (checkbox)
      $livres = $this->getDoctrine()->getRepository(Livre::class)->findAll();
      
      $form = $this->createFormBuilder()
          ->add('liv_num', EntityType::class, [
                      'class' => Livre::class,
                      'multiple' => false,
                      'expanded' => false,
                      'choice_label' => 'liv_titre',
                  ])
          ->add('save', SubmitType::class, array('label' => 'Update livre'))
          ->getForm();
    • Traitement des données du formulaire
      $form->handleRequest($request);
      if ($form->isSubmitted() && $form->isValid()) {
          $livre = $this->getDoctrine()
              ->getRepository(Livre::class)
              ->find($form->getData()['liv_num']);
          $auteur->addLivre($livre);
          $entityManager = $this->getDoctrine()->getManager();
          $entityManager->persist($livre);
          $entityManager->persist($auteur);
          $entityManager->flush();
          return $this->redirectToRoute('detailAuteur', array('id'=> $autId));
          }
    • Demo

    SQL et Repository

    • Doctrine permet de transmettre des requêtes SQL aux entités
      /**
       * Livre
       *
       * @ORM\Table(name="livre")
       * @ORM\Entity
       * @ORM\Entity(repositoryClass="AppBundle\Repository\LivreRepository")
       */
      class Livre
      {
    • Les EntityRepository permettent d'associer des méthodes aux requêtes
      namespace AppBundle\Repository;
      
      use Doctrine\ORM\EntityRepository;
      
      class LivreRepository extends EntityRepository
      {
          public function findAllOrderedByLivTitre()
          {
              return $this->getEntityManager()
                  ->createQuery(
                      'SELECT l FROM AppBundle:Livre l ORDER BY l.livTitre ASC'
                  )
                  ->getResult();
          }
      
          public function findAllOrderedBy(string $colname, $sort = 'ASC') {
              return $this->getEntityManager()
                  ->createQuery(
                      'SELECT l FROM AppBundle:Livre l ORDER BY l.'.$colname.' '.$sort
                  )
                  ->getResult();
              }
      
      } 

    Authentification

    • Symfony intègre également des outils permettant d'identifier les utlisateurs et ainsi de contrôler l'accès des routes.
    • Dans app/config/security.yml on peut paramétrer le contrôle d'accès :
      • # app/config/security.yml
        security:
            providers:
                in_memory:
                    memory: ~
        
            firewalls:
                dev:
                    pattern: ^/(_(profiler|wdt)|css|images|js)/
                    security: false
        
                main:
                     anonymous : ~
      • le firewal dev concerne l'accès aux outils de développement la route /_profiler par exemple ;
      • Les autres urls sont gérées par le firewal main, ici tout est accessible en mode anonyme c'est à dire sans authentification.
    • Un premier mode d'authentification : à l'ancienne via une popup
      • On rajoute ce mode d'authentification dans le fichier security.yml :
        security:
            # ...
            firewalls:
                # ...
                main:
                    anonymous : ~
                    http_basic: ~
      • On construit une nouvelle route d'administration :
        // src/AppBundle/Controller/DefaultController.php
        // ...
        
        use Symfony\Bundle\FrameworkBundle\Controller\Controller;
        use Symfony\Component\HttpFoundation\Response;
        use Symfony\Component\Routing\Annotation\Route;
        
        class DefaultController extends Controller
        {
            /**
             * @Route("/admin")
             */
            public function adminAction()
            {
                return new Response('Admin page!');
            }
        }
      • on complète security.yml :
        # app/config/security.yml
        security:
            # ...
            firewalls:
                # ...
                main:
                    # ...
        
            access_control:
                # require ROLE_ADMIN for /admin*
                - { path: ^/admin, roles: ROLE_ADMIN }
      • On configure les utilisateurs :
        # app/config/security.yml
        security:
            providers:
                in_memory:
                    memory:
                        users:
                            lambda:
                                password: lambdapass
                                roles: 'ROLE_USER'
                            admin:
                                password: teapot
                                roles: 'ROLE_ADMIN'
            # ...
      • Ce n'est pas suffisant ! il faut encore préciser l'encodage des mots de passe :
        # app/config/security.yml
        security:
            # ...
        
            encoders:
                Symfony\Component\Security\Core\User\User: plaintext
            # ...
      • Cryptage du mot de passe :
        # app/config/security.yml
        security:
            # ...
        
            encoders:
                Symfony\Component\Security\Core\User\User:
                    algorithm: bcrypt
                    cost: 12
      • On encode les mots de passe avec la console :
        $ php bin/console security:encode-password
        
        Symfony Password Encoder Utility
        ================================
        
         Type in your password to be encoded:
         >
        
         ------------------ ---------------------------------------------------------------
          Key                Value
         ------------------ ---------------------------------------------------------------
          Encoder used       Symfony\Component\Security\Core\Encoder\BCryptPasswordEncoder
          Encoded password   $2y$12$68BvHejD/f4RKrSnXP7xsuTYGXGmQfcD.O/xwG7q64hXbXmgluPd2
         ------------------ ---------------------------------------------------------------
        
         ! [NOTE] Self-salting encoder used: the encoder generated its own built-in salt.
        
        
         [OK] Password encoding succeeded
        
      • On met à jour le fichier security.yml :
            # https://symfony.com/doc/current/security.html#b-configuring-how-users-are-loaded
            providers:
                in_memory:
                    memory:
                        users:
                            lambda:
                                password: $2y$12$68BvHejD/f4RKrSnXP7xsuTYGXGmQfcD.O/xwG7q64hXbXmgluPd2
                                roles: 'ROLE_USER'
                            admin:
                                password: $2y$12$RGQrY.D3VoeRVxDuL9lHE.Bf3Hj2z8RSFDNMinEk6xfT7SjPTeFJe
                                roles: 'ROLE_ADMIN'
                #...
      • C'est prêt !
    • Quelques inconvénients demeurent : on ne peut pas se déconnecter, les popups c'est môche, on a déjà d'autres systèmes qui gère des utilisateurs, ...
    • Besoin #1 : hiérarchiser les rôles
      # app/config/security.yml
      security:
          # ...
      
          role_hierarchy:
                  ROLE_ADMIN:       ROLE_USER
      Attention tous les noms de rôle doivent commencer par ROLE_
    • Besoin #2 : Gestion du contrôle d'accès
      # app/config/security.yml
      security:
          # ...
          access_control:
              # require ROLE_ADMIN for /admin*
              - { path: ^/admin, roles: ROLE_ADMIN }
              - { path: ^/lambda, roles: ROLE_USER }
              - { path: ^/lmsf, roles: ROLE_USER }
      Attention filtre les routes dans l'ordre de déclaration et adopte la première politique rencontrée
    • Besoin #3 : gestion du contrôle d'accès au sein du controleur
      public function helloAction($name)
      {
          // The second parameter is used to specify on what object the role is tested.
          $this->denyAccessUnlessGranted('ROLE_ADMIN', null, 'Unable to access this page!');
          //...
      }
      ou bien à l'aide d'annotation
      // ...
      use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
      
      /**
       * @Security("has_role('ROLE_ADMIN')")
       */
      public function helloAction($name)
      {
          // ...
      }
      Génère une Réponse HTTP 403 si l'authentification n'est pas validée
    • Besoin #4 : dans les template
      {% if is_granted('ROLE_ADMIN') %}
          <a href="...">Delete</a>
      {% endif %}
      La fonction interne is_granted() fait le job !
    • Besoin #5 : Créer un formulaire de login : Doc symfony

    /bin/console

    • La console offre plein de possibilités...
      $ bin/console --help
      Usage:
        list [options] [--] [<namespace>]
      
      Arguments:
        namespace            The namespace name
      
      Options:
            --raw            To output raw command list
            --format=FORMAT  The output format (txt, xml, json, or md) [default: "txt"]
      
      Help:
        The list command lists all commands:
      
          php bin/console list
      
        You can also display the commands for a specific namespace:
      
          php bin/console list test
      
        You can also output the information in other formats by using the --format option:
      
          php bin/console list --format=xml
      
        It's also possible to get raw list of commands (useful for embedding command runner):
      
          php bin/console list --raw
      
    • Détail des commandes disponibles
      $ bin/console list
      Symfony 3.4.4 (kernel: app, env: dev, debug: true)
      
      Usage:
        command [options] [arguments]
      
      Options:
        -h, --help            Display this help message
        -q, --quiet           Do not output any message
        -V, --version         Display this application version
            --ansi            Force ANSI output
            --no-ansi         Disable ANSI output
        -n, --no-interaction  Do not ask any interactive question
        -e, --env=ENV         The Environment name. [default: "dev"]
            --no-debug        Switches off debug mode.
        -v|vv|vvv, --verbose  Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug
      
      Available commands:
        about                                   Displays information about the current project
        help                                    Displays help for a command
        list                                    Lists commands
       assets
        assets:install                          Installs bundles web assets under a public directory
       cache
        cache:clear                             Clears the cache
        cache:pool:clear                        Clears cache pools
        cache:pool:prune                        Prune cache pools
        cache:warmup                            Warms up an empty cache
       config
        config:dump-reference                   Dumps the default configuration for an extension
       debug
        debug:autowiring                        Lists classes/interfaces you can use for autowiring
        debug:config                            Dumps the current configuration for an extension
        debug:container                         Displays current services for an application
        debug:event-dispatcher                  Displays configured listeners for an application
        debug:form                              Displays form type information
        debug:router                            Displays current routes for an application
        debug:swiftmailer                       [swiftmailer:debug] Displays current mailers for an application
        debug:twig                              Shows a list of twig functions, filters, globals and tests
       doctrine
        doctrine:cache:clear-collection-region  Clear a second-level cache collection region.
        doctrine:cache:clear-entity-region      Clear a second-level cache entity region.
        doctrine:cache:clear-metadata           Clears all metadata cache for an entity manager
        doctrine:cache:clear-query              Clears all query cache for an entity manager
        doctrine:cache:clear-query-region       Clear a second-level cache query region.
        doctrine:cache:clear-result             Clears result cache for an entity manager
        doctrine:cache:contains                 Check if a cache entry exists
        doctrine:cache:delete                   Delete a cache entry
        doctrine:cache:flush                    [doctrine:cache:clear] Flush a given cache
        doctrine:cache:stats                    Get stats on a given cache provider
        doctrine:database:create                Creates the configured database
        doctrine:database:drop                  Drops the configured database
        doctrine:database:import                Import SQL file(s) directly to Database.
        doctrine:ensure-production-settings     Verify that Doctrine is properly configured for a production environment.
        doctrine:generate:crud                  [generate:doctrine:crud] Generates a CRUD based on a Doctrine entity
        doctrine:generate:entities              [generate:doctrine:entities] Generates entity classes and method stubs from your mapping information
        doctrine:generate:entity                [generate:doctrine:entity] Generates a new Doctrine entity inside a bundle
        doctrine:generate:form                  [generate:doctrine:form] Generates a form type class based on a Doctrine entity
        doctrine:mapping:convert                [orm:convert:mapping] Convert mapping information between supported formats.
        doctrine:mapping:import                 Imports mapping information from an existing database
        doctrine:mapping:info
        doctrine:query:dql                      Executes arbitrary DQL directly from the command line.
        doctrine:query:sql                      Executes arbitrary SQL directly from the command line.
        doctrine:schema:create                  Executes (or dumps) the SQL needed to generate the database schema
        doctrine:schema:drop                    Executes (or dumps) the SQL needed to drop the current database schema
        doctrine:schema:update                  Executes (or dumps) the SQL needed to update the database schema to match the current mapping metadata.
        doctrine:schema:validate                Validate the mapping files.
       generate
        generate:bundle                         Generates a bundle
        generate:command                        Generates a console command
        generate:controller                     Generates a controller
       lint
        lint:twig                               Lints a template and outputs encountered errors
        lint:xliff                              Lints a XLIFF file and outputs encountered errors
        lint:yaml                               Lints a file and outputs encountered errors
       router
        router:match                            Helps debug routes by simulating a path info match
       security
        security:check                          Checks security issues in your project dependencies
        security:encode-password                Encodes a password.
       server
        server:log                              Starts a log server that displays logs in real time
        server:run                              Runs a local web server
        server:start                            Starts a local web server in the background
        server:status                           Outputs the status of the local web server for the given address
        server:stop                             Stops the local web server that was started with the server:start command
       swiftmailer
        swiftmailer:email:send                  Send simple email message
        swiftmailer:spool:send                  Sends emails from the spool
              
    • Commandes d'un namespace spécifique
      $ php bin/console list server
      Symfony 3.4.4 (kernel: app, env: dev, debug: true)
      
      Usage:
      command [options] [arguments]
      
      Options:
        -h, --help            Display this help message
        -q, --quiet           Do not output any message
        -V, --version         Display this application version
        --ansi            Force ANSI output
        --no-ansi         Disable ANSI output
        -n, --no-interaction  Do not ask any interactive question
        -e, --env=ENV         The Environment name. [default: "dev"]
        --no-debug        Switches off debug mode.
        -v|vv|vvv, --verbose  Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug
      
        Available commands for the "server" namespace:
        server:log     Starts a log server that displays logs in real time
        server:run     Runs a local web server
        server:start   Starts a local web server in the background
        server:status  Outputs the status of the local web server for the given address
        server:stop    Stops the local web server that was started with the server:start command