Cours de PHP 5

Image non disponible


précédentsommairesuivant

VIII. Exemples d'application

VIII-A. Introduction

Les exemples qui suivent sont tous bâtis sur le même modèle, à savoir un site de tutoriels. Nous verrons plusieurs manières d'aborder cette thématique, les avantages et inconvénients de chaque méthode, et les conclusions qui s'imposent. Attention, l'ergonomie n'est pas au programme : il faudra utiliser vos propres dons ou vous inspirer d'une autre source.

VIII-B. Application simple

VIII-B-1. Introduction

Ce site est fait le plus simplement possible, chaque script étant indépendant des autres.

Arborescence sous "C:\Web\online\http\cours-php\1-simple" (DocumentRoot) :
  • /
  • /index.php
  • /cours.php
  • /auteur.php

VIII-B-2. Les scripts

index.php
Sélectionnez
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="fr-FR" lang="fr-FR">
<head>
    <title>G. Rossolini - Tutoriels PHP</title>
    <meta http-equiv="content-type" content="text/html; charset=utf-8" />
</head>
<body>

<ul style="text-align: center">
    <li style="display: inline"><a
        href="./"
        title="G. Rossolini - Tutoriels PHP">Accueil</a> -</li>
    <li style="display: inline"><a
        href="./articles.php"
        title="G. Rossolini - Liste des publications">Publications</a> -</li>
    <li style="display: inline"><a
        href="./auteurs.php"
        title="G. Rossolini - Liste des auteurs">Auteurs</a></li>
</ul>
<h1 style="text-align: center">Guillaume Rossolini - Tutoriels PHP</h1>

<h2 style="text-decoration: underline">Présentation</h2>
<p>J'ai suivi une formation dans plusieurs domaines, notamment la communication,
    le développement de logiciels, la gestion de projet,
    <span style="font-style: italic">etc</span>.</p>
<p>Aujoud'hui, je suis un développeur PHP muni de la certification Zend :<br />
<a
    href="http://www.zend.com/store/education/certification/authenticate.php?ClientCandidateID=ZEND005053&RegistrationID=221302133"
    title="Zend Certified Engineer Details"><img
        src="http://www.zend.com/images/training/php5_zce_logo.gif"
        alt="Logo ZCE"
        style="border: none"/></a></p>

<p style="text-align: center; font-size: 0.7em;">Copyright
    Guillaume Rossolini 2007-<?php echo date('Y'); ?><br />
Contact : <a
    href="mailto:g-rossolini@developpez.com"
    title="E-mail">g-rossolini@developpez.com</a></p>
</body>
</html>
articles.php
Sélectionnez
<?php
mysql_connect('localhost', 'utilisateur', 'motdepasse');
mysql_select_db('developpez');

function html($string)
{
    return utf8_encode(htmlspecialchars($string, ENT_QUOTES));
}
?>
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="fr-FR" lang="fr-FR">
<head>
    <title>G. Rossolini - Articles</title>
    <meta http-equiv="content-type" content="text/html; charset=utf-8" />
</head>
<body>

<ul style="text-align: center">
    <li style="display: inline"><a
        href="./"
        title="G. Rossolini - Tutoriels PHP">Accueil</a> -</li>
    <li style="display: inline"><a
        href="./articles.php"
        title="G. Rossolini - Liste des publications">Publications</a> -</li>
    <li style="display: inline"><a
        href="./auteurs.php"
        title="G. Rossolini - Liste des auteurs">Auteurs</a></li>
</ul>

<?php
if(empty($_GET['id']))
{
    ?>
    <h1 style="text-align: center">Publications</h1>
    <?php
    $sql = 'SELECT c.id, c.name, COUNT(*) AS nb_articles
            FROM category AS c
            INNER JOIN article AS a ON a.category_id = c.id
            GROUP BY c.id, c.name
            ORDER BY c.name';
    $db_categories = mysql_query($sql);
    while($category = mysql_fetch_assoc($db_categories))
    {
        ?><h2><?php echo html($category['name']); ?></h2><?php
        $sql = 'SELECT ar.id, ar.title,
                    au.id AS author_id, au.name AS author_name
                FROM article AS ar
                INNER JOIN author AS au ON ar.author_id = au.id
                WHERE ar.category_id = %d
                ORDER BY ar.title';
        $db_article = mysql_query(sprintf($sql, $category['id']));
        ?><ul><?php
        while($article = mysql_fetch_assoc($db_article))
        {
            ?>
                <li><a href="./articles.php?id=<?php
                    echo (int)$article['id'];
                ?>" title="<?php echo html($article['title']); ?>"><?php
                    echo html($article['title']);
                ?></a>, par <a href="./auteurs.php?id=<?php
                    echo (int)$article['author_id'];
                ?>" title=""><?php
                    echo html($article['author_name']);
                ?></a></li>
            <?php
        }
        ?></ul><?php
    }
}
else
{
    $id = (int)$_GET['id'];
    $sql = 'SELECT ar.title, ar.text,
                au.id AS author_id, au.name AS author_name
            FROM article AS ar
            INNER JOIN author AS au ON ar.author_id = au.id
            WHERE ar.id = %d';
    $db_article = mysql_query(sprintf($sql, $id));
    if($article = mysql_fetch_assoc($db_article))
    {
        $article['text'] = utf8_decode($article['text']);
        ?>
        <h1 style="text-align: center"><?php
            echo html($article['title']);
        ?></h1>
        <div style="text-align: center; font-style: italic">Par <a
            href="./auteurs.php?id=<?php
                echo html($article['author_id']);
            ?>"
            title="Publications de <?php
                echo html($article['author_name']);
            ?>"><?php
                echo html($article['author_name']);
            ?></a></div>
        <?php
        echo nl2br(html($article['text']));
    }
    else
    {
        echo "Cet article n'existe pas...";
    }
}
?>

<p style="text-align: center; font-size: 0.7em;">Copyright
    Guillaume Rossolini 2007-<?php echo date('Y'); ?><br />
Contact : <a
    href="mailto:g-rossolini@developpez.com"
    title="E-mail">g-rossolini@developpez.com</a></p>
</body>
</html>
auteurs.php
Sélectionnez
<?php
mysql_connect('localhost', 'utilisateur', 'motdepasse');
mysql_select_db('developpez');

function html($string)
{
    return utf8_encode(htmlspecialchars($string, ENT_QUOTES));
}
?>
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="fr-FR" lang="fr-FR">
<head>
    <title>G. Rossolini - Publications</title>
    <meta http-equiv="content-type" content="text/html; charset=utf-8" />
</head>
<body>

<ul style="text-align: center">
    <li style="display: inline"><a
        href="./"
        title="G. Rossolini - Tutoriels PHP">Accueil</a> -</li>
    <li style="display: inline"><a
        href="./articles.php"
        title="G. Rossolini - Liste des publications">Publications</a> -</li>
    <li style="display: inline"><a
        href="./auteurs.php"
        title="G. Rossolini - Liste des auteurs">Auteurs</a></li>
</ul>

<?php
if(empty($_GET['id']))
{
    ?>
    <h1 style="text-align: center">Auteurs</h1>
    <ul style="list-style-type: none">
    <?php
    $sql = 'SELECT au.id, au.name, COUNT(*) AS nb_articles
            FROM author AS au
            LEFT JOIN article AS ar ON ar.author_id = au.id
            GROUP BY au.id, au.name
            ORDER BY name';

    $db_authors = mysql_query($sql);
    while($author = mysql_fetch_assoc($db_authors))
    {
        ?><li><a
            href="./auteurs.php?id=<?php echo (int)$author['id']; ?>"
            title="<?php echo html($author['name']); ?>"><?php
                echo html($author['name']);
            ?></a> (<?php
                echo (int)$author['nb_articles'];
            ?> articles)</li><?php
    }
    ?>
    </ul>
    <?php
}
else
{
    $id = (int)$_GET['id'];
    $sql = 'SELECT name
            FROM author
            WHERE id = %d';

    $db_author = mysql_query(sprintf($sql, $id));
    if($author = mysql_fetch_assoc($db_author))
    {
        ?>
        <h1 style="text-align: center">Publications de <?php
            echo html($author['name']);
        ?></h1>
        <ul>
        <?php
        $sql = 'SELECT id, title
                FROM article
                WHERE author_id = %d
                ORDER BY title';

        $db_article = mysql_query(sprintf($sql, $id));
        while($article = mysql_fetch_assoc($db_article))
        {
            ?><li><a
                href="./articles.php?id=<?php echo html($article['id']); ?>"
                title="Article par <?php echo html($author['name']); ?>"><?php
                    echo html($article['title']);
                ?></a></li><?php
        }
        ?>
        </ul>
        <?php
    }
    else
    {
        echo "Cet auteur n'existe pas...";
    }
}
?>

<p style="text-align: center; font-size: 0.7em;">Copyright
    Guillaume Rossolini 2007-<?php echo date('Y'); ?><br />
Contact : <a
    href="mailto:g-rossolini@developpez.com"
    title="E-mail">g-rossolini@developpez.com</a></p>
</body>
</html>

Même si tout est parfaitement fonctionnel et que le code XHTML produit est parfaitement standard, voyons maintenant les inconvénients de cette approche, si souvent utilisée par les débutants avant qu'ils prennent de l'expérience.

VIII-B-3. Avantages

Ce code est très rapide à écrire.

Puisque le code de chaque script n'est pas très long, nous avons une bonne vision détaillée de leur comportement (en un clin d'oeil).

VIII-B-4. Inconvénients

L'en-tête et le pied de page (code XHTML) sont répétés dans tous les documents, alors qu'ils sont toujours identiques. Nous pourrions définir des fonctions, mais le plus adéquat est d'inclure des fichiers.

Les paramètres d'accès et la connexion à la base de données sont eux aussi répétés dans plusieurs scripts. Une inclusion serait également bienvenue.

Dans le même ordre d'idée, la fonction html() est déclarée à l'identique dans tous les scripts.

Le dialogue avec la base de données se fait au moyen de l'API standard de MySQL, ce qui est très restrictif. Il serait préférable d'utiliser PDO.

À chaque retour de requête, nous sommes obligés de faire manuellement une conversion d'encodage. Utiliser une classe étendue de PDO nous permettra de nous affranchir de ce problème.

Un problème moins évident est que le terme "php" apparaît dans les URL. Par conséquent, ces URLs sont éligibles pour toutes les recherches sur "php" dans les moteurs de recherche. Dans mon cas ce n'est pas grave puisqu'il s'agit justement de tutoriels PHP, mais imaginons un site sur l'alimentaire ou tout autre sujet en décalage complet avec PHP... En ce cas, l'extension de fichier dans l'URL n'est pas une bonne chose, il est préférable de s'en débarasser. Cela peut même améliorer la sécurité de votre site...

Enfin, il y a bien trop de mélanges de PHP et de XHTML, le code du script est donc très complexe à lire. Imaginez que j'introduise du code CSS, JavaScript etc., le code source de mon script deviendrait rapidement incompréhensible.

L'inconvénient majeur est donc la maintenabilité du code : nous n'avons adopté que très peu de règles d'écriture du code, et par conséquent il est difficile à relire.

VIII-C. Inclusions

VIII-C-1. Introduction

Dans un premier temps, nous allons corriger la répétition du code.

Arborescence sous "C:\Web\online\http\cours-php\2-includes" (DocumentRoot) :
  • /
  • /index.php
  • /articles.php
  • /auteurs.php
Arborescence sous "C:\Web\offline\sites\cours-php\2-includes" (hors de DocumentRoot) :
  • /
  • /footer.html
  • /header.html
  • /common.php
  • /functions.php

Le moyen le plus efficace de rendre les fichiers à inclure accessibles aux scripts du site, est de modifier la configuration d'Apache via le httpd.conf ou .htaccess. Cela nous évitera de mettre tout le chemin vers les fichiers dans tous nos scripts. Voici comment cela se présente dans mon cas :

VIII-C-2. Les scripts

Ajouter à "httpd.conf" :
Sélectionnez
<Directory "C:/Web/online/http/cours-php/2-includes">
    AllowOverride None
    php_value include_path ".;C:/Web/offline/sites/cours-php/2-includes"
</Directory>
index.php
Sélectionnez
<?php
include 'header.tpl';
?>
<h1 style="text-align: center">Guillaume Rossolini - Tutoriels PHP</h1>

<h2 style="text-decoration: underline">Présentation</h2>
<p>J'ai suivi une formation dans plusieurs domaines, notamment la communication,
    le développement de logiciels, la gestion de projet,
    <span style="font-style: italic">etc</span>.</p>
<p>Aujoud'hui, je suis un développeur PHP muni de la certification Zend :<br />
<a href="http://www.zend.com/store/education/certification/authenticate.php?ClientCandidateID=ZEND005053&RegistrationID=221302133"
    title="Zend Certified Engineer Details"><img
        src="http://www.zend.com/images/training/php5_zce_logo.gif"
        alt="Logo ZCE"
        style="border: none"/></a></p>
<?php
include 'footer.tpl';
articles.php
Sélectionnez
<?php
require_once 'common.php';
require_once 'functions.php';
include 'header.tpl';

if(empty($_GET['id']))
{
    ?>
    <h1 style="text-align: center">Publications</h1>
    <?php
    $sql = 'SELECT c.id, c.name, COUNT(*) AS nb_articles
            FROM category AS c
            INNER JOIN article AS a ON a.category_id = c.id
            GROUP BY c.id, c.name
            ORDER BY c.name';

    $db_categories = mysql_query($sql) or die(mysql_error());
    while($category = mysql_fetch_assoc($db_categories))
    {
        ?><h2><?php echo html($category['name']); ?></h2><?php
        $sql = 'SELECT ar.id, ar.title,
                    au.id AS author_id, au.name AS author_name
                FROM article AS ar
                INNER JOIN author AS au ON ar.author_id = au.id
                WHERE ar.category_id = %d
                ORDER BY ar.title';
        $db_article = mysql_query(sprintf($sql, $category['id']));
        ?><ul><?php
        while($article = mysql_fetch_assoc($db_article))
        {
            ?><li><a href="./articles.php?id=<?php echo (int)$article['id']; ?>"
                title="<?php echo html($article['title']); ?>"><?php
                    echo html($article['title']);
                ?></a>, par <a href="./auteurs.php?id=<?php
                    echo (int)$article['author_id'];
                ?>" title=""><?php
                    echo html($article['author_name']);
                ?></a></li><?php
        }
        ?></ul><?php
    }
}
else
{
    $id = (int)$_GET['id'];
    $sql = 'SELECT ar.title, ar.text,
                au.id AS author_id, au.name AS author_name
            FROM article AS ar
            INNER JOIN author AS au ON ar.author_id = au.id
            WHERE ar.id = %d';

    $db_article = mysql_query(sprintf($sql, $id));
    if($article = mysql_fetch_assoc($db_article))
    {
        $article['text'] = utf8_decode($article['text']);
        ?>
        <h1 style="text-align: center"><?php
            echo html($article['title']);
        ?></h1>
        <div style="text-align: center; font-style: italic">Par <a
            href="./auteurs.php?id=<?php echo (int)$article['author_id']; ?>"
            title="Publications de <?php
                echo html($article['author_name']);
            ?>"><?php echo html($article['author_name']); ?></a></div>
        <?php
        echo nl2br(html($article['text']));
    }
    else
    {
        echo "Cet article n'existe pas...";
    }
}

include 'footer.tpl';
auteurs.php
Sélectionnez
<?php
require_once 'common.php';
require_once 'functions.php';
include 'header.tpl';

if(empty($_GET['id']))
{
    ?>
    <h1 style="text-align: center">Auteurs</h1>
    <ul style="list-style-type: none">
    <?php
    $sql = 'SELECT au.id, au.name, COUNT(*) AS nb_articles
            FROM author AS au
            LEFT JOIN article AS ar ON ar.author_id = au.id
            GROUP BY au.id, au.name
            ORDER BY name';

    $db_authors = mysql_query($sql);
    while($author = mysql_fetch_assoc($db_authors))
    {
        ?><li><a
            href="./auteurs.php?id=<?php echo (int)$author['id']; ?>"
            title="<?php echo html($author['name']); ?>"><?php
                echo html($author['name']);
            ?></a> (<?php
                echo (int)$author['nb_articles'];
            ?> articles)</li><?php
    }
    ?>
    </ul>
    <?php
}
else
{
    $id = (int)$_GET['id'];
    $sql = 'SELECT name
            FROM author
            WHERE id = %d';

    $db_author = mysql_query(sprintf($sql, $id));
    if($author = mysql_fetch_assoc($db_author))
    {
        ?>
        <h1 style="text-align: center">Publications de <?php
            echo html($author['name']);
        ?></h1>
        <ul>
        <?php
        $sql = 'SELECT id, title
                FROM article
                WHERE author_id = %d
                ORDER BY title';

        $db_article = mysql_query(sprintf($sql, $id));
        while($article = mysql_fetch_assoc($db_article))
        {
            ?><li><a
                href="./articles.php?id=<?php echo (int)$article['id']; ?>"
                title="Article par <?php
                    echo html($author['name']);
                ?>"><?php echo html($article['title']); ?></a></li><?php
        }
        ?>
        </ul>
        <?php
    }
    else
    {
        echo "Cet auteur n'existe pas...";
    }
}

include 'footer.tpl';
common.php
Sélectionnez
<?php
mysql_connect('localhost', 'utilisateur', 'motdepasse');
mysql_select_db('developpez');
functions.php
Sélectionnez
<?php
function html($string)
{
    return utf8_encode(htmlspecialchars($string, ENT_QUOTES));
}
header.html
Sélectionnez
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="fr-FR" lang="fr-FR">
<head>
    <title>G. Rossolini - Tutoriels PHP</title>
    <meta http-equiv="content-type" content="text/html; charset=utf-8" />
</head>
<body>

<ul style="text-align: center">
    <li style="display: inline"><a
        href="./"
        title="G. Rossolini - Tutoriels PHP">Accueil</a> -</li>
    <li style="display: inline"><a
        href="./articles.php"
        title="G. Rossolini - Liste des publications">Publications</a> -</li>
    <li style="display: inline"><a
        href="./auteurs.php"
        title="G. Rossolini - Liste des auteurs">Auteurs</a></li>
</ul>
footer.html
Sélectionnez

<p style="text-align: center; font-size: 0.7em;">Copyright
    Guillaume Rossolini 2007-<?php echo date('Y'); ?><br />
Contact : <a
    href="mailto:g-rossolini@developpez.com"
    title="E-mail">g-rossolini@developpez.com</a></p>
</body>
</html>

VIII-C-3. Nouveaux avantages

Toute modification de l'en-tête ou du pied de page est immédiatement répercutée dans toutes les pages du site.

Puisqu'ils sont hors du DocumentRoot, les fichiers à inclure sont protégés de tout accès externe. Leur chemin d'accès étant dans l'include_path de PHP, ils sont également facilement accessibles.

VIII-C-4. Nouveaux inconvénients

Les fichiers à inclure ne sont plus intégralement copiés dans les scripts effectifs, mais l'on se rend compte que les instructions require* et include* se répètent d'un script à l'autre. Nous verrons plus loin que MVC nous permet d'éviter cette situation.

Par exemple, si je souhaite conserver des statistiques sur mes visiteurs, une solution serait de créer un script "stats.php" à inclure dans toutes mes pages. Je serais donc obligé de répéter ce code dans tous les scripts :

 
Sélectionnez
<?php
require_once 'common.php';
require_once 'functions.php';
include 'stats.php';
include 'header.tpl';

Un problème évident avec le code ci-dessus est que nous pourrions oublier d'inclure "stats.php" dnas l'un des scripts... Un autre est que le bloc d'includes/requires est tout simplement du code répété, or il nous faut éviter ce genre de situations.

VIII-D. Sans l'extension ".php"

VIII-D-1. Introduction

Nous avons déjà établi que l'extension ".php" est un problème dans l'URL. Maintenant que notre menu d'en-tête est géré par des inclusions, nous pouvons plus facilement modifier les liens du site.

Le principe est d'utiliser des URLs absolues à partir de la racine du site, donc commençant par "/" plutôt que "./", pour chacun des liens. Nous allons mettre chaque script dans un répertoire, ce qui nous permettra de l'appeler "index.php" et ainsi de nous passer totalement du nom du script dans l'URL. Un nouveau fichier de configuration permet de respecter DRY (don't repeat yourself).

Arborescence sous "C:\Web\online\http\cours-php\3-extensions" (DocumentRoot) :
  • /
  • /index.php
  • /articles/index.php
  • /auteurs/index.php
Arborescence sous "C:\Web\offline\sites\cours-php\3-extensions" (hors de DocumentRoot) :
  • /
  • /footer.html
  • /header.html
  • /common.php
  • /functions.php

VIII-D-2. Les scripts modifiés

Ajouter à "httpd.conf" :
Sélectionnez
<Directory "C:/Web/online/http/cours-php/3-extensions">
    AllowOverride None
    php_value include_path ".;C:/Web/offline/sites/cours-php/3-extensions"
    SetEnv HTTP_ROOT /cours-php/3-extensions/
</Directory>
header.html
Sélectionnez
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="fr-FR" lang="fr-FR">
<head>
    <title>G. Rossolini - Tutoriels PHP</title>
    <meta http-equiv="content-type" content="text/html; charset=utf-8" />
</head>
<body>

<ul style="text-align: center">
    <li style="display: inline"><a
        href="<?php echo html($_SERVER['HTTP_ROOT']); ?>"
        title="G. Rossolini - Tutoriels PHP">Accueil</a> -</li>
    <li style="display: inline"><a
        href="<?php echo html($_SERVER['HTTP_ROOT']); ?>articles/"
        title="G. Rossolini - Liste des publications">Publications</a> -</li>
    <li style="display: inline"><a
        href="<?php echo html($_SERVER['HTTP_ROOT']); ?>auteurs/"
        title="G. Rossolini - Liste des auteurs">Auteurs</a></li>
</ul>
articles/index.php
Sélectionnez
<?php
require_once 'common.php';
require_once 'functions.php';
include 'header.tpl';

if(empty($_GET['id']))
{
    ?>
    <h1 style="text-align: center">Publications</h1>
    <?php
    $sql = 'SELECT c.id, c.name, COUNT(*) AS nb_articles
            FROM category AS c
            INNER JOIN article AS a ON a.category_id = c.id
            GROUP BY c.id, c.name
            ORDER BY c.name';

    $db_categories = mysql_query($sql) or die(mysql_error());
    while($category = mysql_fetch_assoc($db_categories))
    {
        ?><h2><?php echo html($category['name']); ?></h2><?php
        $sql = 'SELECT ar.id, ar.title,
                    au.id AS author_id, au.name AS author_name
                FROM article AS ar
                INNER JOIN author AS au ON ar.author_id = au.id
                WHERE ar.category_id = %d
                ORDER BY ar.title';

        $db_article = mysql_query(sprintf($sql, $category['id']));
        ?><ul><?php
        while($article = mysql_fetch_assoc($db_article))
        {
            ?><li><a href="<?php
                echo html($_SERVER['HTTP_ROOT']);
            ?>articles/?id=<?php
                echo (int)$article['id'];
            ?>" title="<?php
                echo html($article['title']);
            ?>"><?php
                echo html($article['title']);
            ?></a>, par <a href="<?php
                echo html($_SERVER['HTTP_ROOT']);
            ?>auteurs/?id=<?php
                echo (int)$article['author_id'];
            ?>" title=""><?php
                echo html($article['author_name']);
            ?></a></li><?php
        }
        ?></ul><?php
    }
}
else
{
    $id = (int)$_GET['id'];
    $sql = 'SELECT ar.title, ar.text,
                au.id AS author_id, au.name AS author_name
            FROM article AS ar
            INNER JOIN author AS au ON ar.author_id = au.id
            WHERE ar.id = %d';

    $db_article = mysql_query(sprintf($sql, $id));
    if($article = mysql_fetch_assoc($db_article))
    {
        $article['text'] = utf8_decode($article['text']);
        ?>
        <h1 style="text-align: center"><?php
            echo html($article['title']);
        ?></h1>
        <div style="text-align: center; font-style: italic">Par <a href="<?php
            echo html($_SERVER['HTTP_ROOT']);
        ?>auteurs/?id=<?php
            echo (int)$article['author_id'];
        ?>" title="Publications de <?php
            echo html($article['author_name']);
        ?>"><?php
            echo html($article['author_name']);
        ?></a></div>
        <?php
        echo nl2br(html($article['text']));
    }
    else
    {
        echo "Cet article n'existe pas...";
    }
}

include 'footer.tpl';
auteurs/index.php
Sélectionnez
<?php
require_once 'common.php';
require_once 'functions.php';
include 'header.tpl';

if(empty($_GET['id']))
{
    ?>
    <h1 style="text-align: center">Auteurs</h1>
    <ul style="list-style-type: none">
    <?php
    $sql = 'SELECT au.id, au.name, COUNT(*) AS nb_articles
            FROM author AS au
            LEFT JOIN article AS ar ON ar.author_id = au.id
            GROUP BY au.id, au.name
            ORDER BY name';

    $db_authors = mysql_query($sql);
    while($author = mysql_fetch_assoc($db_authors))
    {
        ?><li><a href="<?php
            echo html($_SERVER['HTTP_ROOT']);
        ?>auteurs/?id=<?php
            echo (int)$author['id'];
        ?>" title="<?php
            echo html($author['name']);
        ?>"><?php
            echo html($author['name']);
        ?></a> (<?php
            echo (int)$author['nb_articles'];
        ?> articles)</li><?php
    }
    ?>
    </ul>
    <?php
}
else
{
    $id = (int)$_GET['id'];
    $sql = 'SELECT name
            FROM author
            WHERE id = %d';

    $db_author = mysql_query(sprintf($sql, $id));
    if($author = mysql_fetch_assoc($db_author))
    {
        ?>
        <h1 style="text-align: center">Publications de <?php
            echo html($author['name']);
        ?></h1>
        <ul>
        <?php
        $sql = 'SELECT id, title
                FROM article
                WHERE author_id = %d
                ORDER BY title';

        $db_article = mysql_query(sprintf($sql, $id));
        while($article = mysql_fetch_assoc($db_article))
        {
            ?><li><a href="<?php
                echo html($_SERVER['HTTP_ROOT']);
            ?>articles/?id=<?php
                echo (int)$article['id'];
            ?>" title="Article par <?php
                echo html($author['name']);
            ?>"><?php
                echo html($article['title']);
            ?></a></li><?php
        }
        ?>
        </ul>
        <?php
    }
    else
    {
        echo "Cet auteur n'existe pas...";
    }
}

include 'footer.tpl';

VIII-D-3. Nouveaux avantages

L'extension ".php" n'apparaît plus dans l'URL, les moteurs de recherche peuvent donc référencer la totalité de nos URLs sans trouver de mot inutile.

VIII-D-4. Nouveaux inconvénients

Nous avons maintenant autant de répertoires publics que de scripts, ce qui n'est pas nécessairement très pratique ou utile pour notre organisation interne.

VIII-E. Modèles (classes pour la BDD)

VIII-E-1. Introduction

Arborescence sous "C:\Web\online\http\cours-php\4-modeles" (DocumentRoot) :
  • /
  • /index.php
  • /articles.php
  • /auteurs.php
Arborescence sous "C:\Web\offline\sites\cours-php\4-modeles" (hors de DocumentRoot) :
  • /
  • /AbstractTable.php
  • /Article.php
  • /Author.php
  • /Category.php
  • /common.php
  • /footer.html
  • /functions.php
  • /header.html
  • /MyPDO.php

VIII-E-2. Les scripts

Ajouter à "httpd.conf" :
Sélectionnez
<Directory "C:/Web/online/http/cours-php/4-modeles">
    AllowOverride None
    php_value include_path ".;C:/Web/offline/sites/cours-php/4-modeles"
    SetEnv HTTP_ROOT /cours-php/4-modeles/
</Directory>
AbstractTable.php
Sélectionnez
<?php
abstract class AbstractTable
{
    protected $db;
    protected $selectAll;
    protected $selectById;

    public function __construct($db)
    {
        $this->db = $db;
    }

    public function getAll()
    {
        $this->selectAll->execute();
        return $this->selectAll->fetchAll();
    }

    public function getById($id)
    {
        $this->selectById->execute(array($id));
        $row = $this->selectById->fetchAll();
        if(empty($row[0]))
        {
            return array();
        }
        else
        {
            return $row[0];
        }
    }
}
common.php
Sélectionnez
<?php
spl_autoload_register('generic_autoload');
$db = new MyPDO(
    'mysql:host=localhost;dbname=developpez', 'utilisateur', 'motdepasse');
functions.php
Sélectionnez
<?php
function html($string)
{
    return utf8_encode(htmlspecialchars($string, ENT_QUOTES));
}

function generic_autoload($class)
{
    require_once $class.'.php';
}
MyPDO.php
Sélectionnez
<?php
class MyPDO extends PDO
{
    public function __construct($dsn, $user=NULL, $password=NULL)
    {
        parent::__construct($dsn, $user, $password);
        $this->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
    }

    public function prepare($sql, $options=NULL)
    {
        $statement = parent::prepare($sql);
        if(strpos(strtoupper($sql), 'SELECT') === 0) //requête "SELECT"
        {
            $statement->setFetchMode(PDO::FETCH_ASSOC);
        }

        return $statement;
    }
}
Article.php
Sélectionnez
<?php
class Article extends AbstractTable
{
    protected $selectById;
    protected $selectByAuthor;
    protected $selectByCategory;

    public function __construct($db)
    {
        parent::__construct($db);

        $sql = 'SELECT ar.title, ar.text,
                    au.id AS author_id, au.name AS author_name
                FROM article AS ar
                INNER JOIN author AS au ON ar.author_id = au.id
                WHERE ar.id = ?';
        $this->selectById = $this->db->prepare($sql);

        $sql = 'SELECT id, title
                FROM article
                WHERE author_id = ?
                ORDER BY title';
        $this->selectByAuthor = $this->db->prepare($sql);

        $sql = 'SELECT ar.id, ar.title,
                    au.id AS author_id, au.name AS author_name
                FROM article AS ar
                INNER JOIN author AS au ON ar.author_id = au.id
                WHERE ar.category_id = ?
                ORDER BY ar.title';
        $this->selectByCategory = $this->db->prepare($sql);
    }

    public function getById($id)
    {
        $this->selectById->execute(array($id));
        $articles = $this->selectById->fetchAll();
        if(empty($articles[0]))
        {
            return array();
        }
        else
        {
            $articles[0]['text'] = utf8_decode($articles[0]['text']);
            return $articles[0];
        }
    }

    public function getByAuthor($id)
    {
        $this->selectByAuthor->execute(array($id));
        return $this->selectByAuthor->fetchAll();
    }

    public function getByCategory($id)
    {
        $this->selectByCategory->execute(array($id));
        return $this->selectByCategory->fetchAll();
    }
}
Author.php
Sélectionnez
<?php
class Author extends AbstractTable
{
    public function __construct($db)
    {
        parent::__construct($db);

        $sql = 'SELECT au.id, au.name, COUNT(*) AS nb_articles
                FROM author AS au
                LEFT JOIN article AS ar ON ar.author_id = au.id
                GROUP BY au.id, au.name
                ORDER BY name';
        $this->selectAll = $this->db->prepare($sql);

        $sql = 'SELECT name
                FROM author
                WHERE id = ?';
        $this->selectById = $this->db->prepare($sql);
    }
}
Category.php
Sélectionnez
<?php
class Category extends AbstractTable
{
    public function __construct($db)
    {
        parent::__construct($db);

        $sql = 'SELECT c.id, c.name, COUNT(*) AS nb_articles
                FROM category AS c
                INNER JOIN article AS a ON a.category_id = c.id
                GROUP BY c.id, c.name
                ORDER BY c.name';
        $this->selectAll = $this->db->prepare($sql);
    }
}
/index.php
Sélectionnez
<?php
require_once 'functions.php';
include 'header.tpl';
?>
<h1 style="text-align: center">Guillaume Rossolini - Tutoriels PHP</h1>

<h2 style="text-decoration: underline">Présentation</h2>
<p>J'ai suivi une formation dans plusieurs domaines, notamment la communication,
    le développement de logiciels, la gestion de projet,
    <span style="font-style: italic">etc</span>.</p>
<p>Aujoud'hui, je suis un développeur PHP muni de la certification Zend :<br />
<a href="http://www.zend.com/store/education/certification/authenticate.php?ClientCandidateID=ZEND005053&RegistrationID=221302133"
    title="Zend Certified Engineer Details"><img
        src="http://www.zend.com/images/training/php5_zce_logo.gif"
        alt="Logo ZCE"
        style="border: none"/></a></p>
<?php
include 'footer.tpl';
/articles/index.php
Sélectionnez
<?php
require_once 'functions.php';
require_once 'common.php';
include 'header.tpl';

$articles = new Article($db);
$categories = new Category($db);

if(empty($_GET['id']))
{
    ?>
    <h1 style="text-align: center">Publications</h1>
    <?php
    foreach($categories->getAll() as $category)
    {
        ?><h2><?php echo html($category['name']); ?></h2><?php
        ?><ul><?php
        foreach($articles->getByCategory($category['id']) as $article)
        {
            ?><li><a href="<?php
                echo html($_SERVER['HTTP_ROOT']);
            ?>articles/?id=<?php
                echo (int)$article['id'];
            ?>" title="<?php
                echo html($article['title']);
            ?>"><?php
                echo html($article['title']);
            ?></a>, par <a href="<?php
                echo html($_SERVER['HTTP_ROOT']);
            ?>auteurs/?id=<?php
                echo (int)$article['author_id'];
            ?>" title=""><?php
                echo html($article['author_name']);
            ?></a></li><?php
        }
        ?></ul><?php
    }
}
else
{
    $id = (int)$_GET['id'];
    if($article = $articles->getById($id))
    {
        ?>
        <h1 style="text-align: center"><?php
            echo html($article['title']);
        ?></h1>
        <div style="text-align: center; font-style: italic">Par <a
            href="./auteurs.php?id=<?php echo (int)$article['author_id']; ?>"
            title="Publications de <?php
                echo html($article['author_name']);
            ?>"><?php echo html($article['author_name']); ?></a></div>
        <?php
        echo nl2br(html($article['text']));
    }
    else
    {
        echo "Cet article n'existe pas...";
    }
}

include 'footer.tpl';
auteurs.php
Sélectionnez
<?php
require_once 'functions.php';
require_once 'common.php';
include 'header.tpl';

$authors = new Author($db);
$articles = new Article($db);

if(empty($_GET['id']))
{
    ?>
    <h1 style="text-align: center">Auteurs</h1>
    <ul style="list-style-type: none">
    <?php
    foreach($authors->getAll() as $author)
    {
        ?><li><a href="<?php
            echo html($_SERVER['HTTP_ROOT']);
        ?>auteurs/?id=<?php
            echo (int)$author['id'];
        ?>" title="<?php echo html($author['name']); ?>"><?php
            echo html($author['name']);
        ?></a> (<?php echo (int)$author['nb_articles']; ?> articles)</li><?php
    }
    ?>
    </ul>
    <?php
}
else
{
    $id = (int)$_GET['id'];
    if($author = $authors->getById($id))
    {
        ?>
        <h1 style="text-align: center">Publications de <?php
            echo html($author['name']);
        ?></h1>
        <ul>
        <?php
        foreach($articles->getByAuthor($id) as $article)
        {
            ?><li><a href="<?php
                echo html($_SERVER['HTTP_ROOT']);
            ?>articles/?id=<?php
                echo (int)$article['id'];
            ?>" title="Article par <?php echo html($author['name']); ?>"><?php
                echo html($article['title']);
            ?></a></li><?php
        }
        ?>
        </ul>
        <?php
    }
    else
    {
        echo "Cet auteur n'existe pas...";
    }
}

include 'footer.tpl';

VIII-E-3. Nouveaux avantages

Le code peut maintenant être porté sans aucun problème d'un SGBD à un autre. De plus, les requêtes sont facilement identifiables puisqu'elles sont regroupées dans des classes, et nous sommes totalement protégés contre les injections SQL.

VIII-E-4. Nouveaux inconvénients

Les scripts à inclure sont tous regroupés et mélangés, il n'y a aucune organisation. Cela peut convenir pour un site de quelques pages mais, à mesure que le site grossit, il devient apparent qu'il faut structurer ces fichiers. Il est temps d'adopter une structure MVC.

Il reste encore beaucoup de mélange PHP/HTML. Heureusement, nous avons effectué de nombreuses mises en facteur du reste du code, et par conséquent ces mélanges PHP/HTML sont à présent tout à fait lisibles.

VIII-F. URL Rewriting, ou Routage

VIII-F-1. Introduction

Nous pouvons maintenant résoudre le problème des URLs d'accès à nos pages. En effet, nous avons vu plus tôt dans ce cours que les URLs à base de paramètres GET visibles sont peu efficaces, tant pour l'oeil humain que pour les robots qui s'occupent de référencer nos sites.

Une solution à ces problèmes est la réécriture de liens, aussi appelée "URL Rewriting" ou encore "routage".

Nous allons utiliser certains aspects avancés d'Apache, et en particulier le module mod_rewrite (qu'il faut donc activer dans httpd.conf). Il nous faudra aussi de bonnes connaissances en expressions régulières (aka "regex" ou "expressions rationnelles").

Le principe de la réécriture de liens est d'utiliser une URL publique ne correspondant pas à un fichier sur le serveur. Le mod_rewrite nous permet de router ces URLs vers les scripts de notre serveur, en interne, c'est-à-dire sans causer de redirection HTTP et sans que le client s'en rende compte. Nous pouvons ainsi choisir les paramètres GET les plus adaptés, sans pour autant encombrer le code de nos scripts PHP.

Arborescence sous "C:\Web\online\http\cours-php\5-routing" (DocumentRoot) :
  • /
  • /index.php
  • /articles.php
  • /auteurs.php
Arborescence sous "C:\Web\offline\sites\cours-php\5-routing" (hors de DocumentRoot) :
  • /
  • /AbstractTable.php
  • /Article.php
  • /Author.php
  • /Category.php
  • /common.php
  • /footer.html
  • /functions.php
  • /header.html
  • /MyPDO.php

Pour pouvoir utiliser la réécriture avec Apache dans un fichier .htaccess, vous aurez besoin d'activer l'option +FollowSymLinks :

 
Sélectionnez
Options +FollowSymLinks
RewriteEngine On

Sans cette option, utiliser le RewriteEngine dans un fichier .htaccess donnera lieu à une erreur HTTP 403 et à une entrée log de ce style :

 
Sélectionnez
Options FollowSymLinks or SymLinksIfOwnerMatch is off
    which implies that RewriteRule directive is forbidden

VIII-F-2. Les scripts

Nous aurons peu de modifications. Les liens doivent maintenant être absolus afin de ne pas porter à confusion ; pour le reste, le httpd.conf est à peu près le seul fichier à modifier. Je vais indiquer ici uniquement les fichiers modifiés :

Ajouter à "httpd.conf" :
Sélectionnez
<Directory "C:/Web/online/http/cours-php/5-routing">
    AllowOverride None
    php_value include_path ".;C:/Web/offline/sites/cours-php/5-routing"
    SetEnv HTTP_ROOT /cours-php/5-routing/
    RewriteEngine on
    RewriteBase /cours-php/5-routing/
    RewriteRule ^auteurs/$ auteurs.php [L]
    RewriteRule ^auteur/([0-9])-?.*$ auteurs.php?id=$1 [L]
    RewriteRule ^articles/$ articles.php [L]
    RewriteRule ^article/([0-9])-?.*$ articles.php?id=$1 [L]
</Directory>
functions.php
Sélectionnez
<?php
function html($string)
{
    return utf8_encode(htmlspecialchars($string, ENT_QUOTES));
}

function url_format($string)
{
    $string = str_replace(' ', '-', strtolower($string));
    return html(preg_replace('#[^a-z0-9-]#', '', $string));
}

function generic_autoload($class)
{
    require_once $class.'.php';
}
auteurs.php
Sélectionnez
<?php
require_once 'functions.php';
require_once 'common.php';
include 'header.tpl';

$authors = new Author($db);
$articles = new Article($db);

if(empty($_GET['id']))
{
    ?>
    <h1 style="text-align: center">Auteurs</h1>
    <ul style="list-style-type: none">
    <?php
    foreach($authors->getAll() as $author)
    {
        ?><li><a href="<?php
            echo html($_SERVER['HTTP_ROOT']);
        ?>auteur/<?php
            echo url_format($author['id'].'-'.$author['name']);
        ?>" title="<?php
            echo html($author['name']);
        ?>"><?php
            echo html($author['name']);
        ?></a> (<?php echo (int)$author['nb_articles']; ?> articles)</li><?php
    }
    ?>
    </ul>
    <?php
}
else
{
    $id = (int)$_GET['id'];
    if($author = $authors->getById($id))
    {
        ?>
        <h1 style="text-align: center">Publications de <?php
            echo html($author['name']);
        ?></h1>
        <ul>
        <?php
        foreach($articles->getByAuthor($id) as $article)
        {
            ?><li><a href="<?php
                echo html($_SERVER['HTTP_ROOT']);
            ?>article/<?php
                echo url_format($article['id'].'-'.$article['title']);
            ?>" title="Article par <?php
                echo html($author['name']);
            ?>"><?php echo html($article['title']); ?></a></li><?php
        }
        ?>
        </ul>
        <?php
    }
    else
    {
        echo "Cet auteur n'existe pas...";
    }
}

include 'footer.tpl';
articles.php
Sélectionnez
<?php
require_once 'functions.php';
require_once 'common.php';
include 'header.tpl';

$articles = new Article($db);
$categories = new Category($db);

if(empty($_GET['id']))
{
    ?>
    <h1 style="text-align: center">Publications</h1>
    <?php
    foreach($categories->getAll() as $category)
    {
        ?><h2><?php echo html($category['name']); ?></h2><?php
        ?><ul><?php
        foreach($articles->getByCategory($category['id']) as $article)
        {
            ?><li><a href="<?php
                echo html($_SERVER['HTTP_ROOT']);
            ?>article/<?php
                echo url_format($article['id'].'-'.$article['title'])
            ?>" title="<?php
                echo html($article['title']);
            ?>"><?php
                echo html($article['title']);
            ?></a>, par <a href="<?php
                echo html($_SERVER['HTTP_ROOT']);
            ?>auteur/<?php
                echo url_format(
                    $article['author_id'].'-'.$article['author_name']);
            ?>" title=""><?php echo html($article['author_name']); ?></a></li><?php
        }
        ?></ul><?php
    }
}
else
{
    $id = (int)$_GET['id'];
    if($article = $articles->getById($id))
    {
        ?>
        <h1 style="text-align: center"><?php
            echo html($article['title']);
        ?></h1>
        <div style="text-align: center; font-style: italic">Par <a
            href="./auteurs.php?id=<?php echo html($article['author_id']); ?>"
            title="Publications de <?php
                echo html($article['author_name']);
            ?>"><?php echo html($article['author_name']); ?></a></div>
        <?php
        echo nl2br(html($article['text']));
    }
    else
    {
        echo "Cet article n'existe pas...";
    }
}

include 'footer.tpl';

VIII-F-3. Nouveaux avantages

VIII-F-4. Nouveaux inconvénients

L'inconvénient majeur est que nous avons maintenant une infinité de points d'entrée pour chaque page. En effet, les anciennes URLs fonctionnent encore : c'est inévitable puisque les scripts auteurs.php et articles.php sont encore accessibles depuis l'extérieur.

Par ailleurs, on peut maintenant mettre n'importe quelle chaîne à la suite des identifiants numériques : la présence de l'identifiant est suffisante pour charger la page. Cela pose de gros problèmes de duplicate content (plusieurs URLs pour une seule ressource).

Ces deux URLs chargent exactement le même document :
  • http://localhost/cours-php/4-routing/article/6-dcouverte-des-principaux-moteurs-de-template-en-php
  • http://localhost/cours-php/4-routing/article/6-alterner-entre-plusieurs-versions-dapache-et-de-php

Dans cet exemple, la 2° URL est fausse puisque le titre de l'article n° 5 est donné à l'URL conduisant à l'article n° 6. Cependant, nous n'avons mis en place aucun mécanisme pour indiquer à l'utilisateur qu'il n'utilise pas la bonne URL. Si nous envoyons encore une redirection HTTP, les navigateurs risquent de déceler une "redirection infinie" et de ne jamais afficher le document souhaité.

VIII-G. Design pattern MVC (modèle-vue-contrôleur)

VIII-G-1. Introduction

Un design pattern permet de résoudre la plupart des inconvénients énoncés ci-dessus : MVC.

L'objectif est d'utiliser des contrôleurs pour traiter les données, des modèles pour les récupérer et des vues pour les afficher. Un contrôleur principal, appelé front controller ou encore contrôleur frontal, est utilisé pour centraliser l'ensemble des requêtes du client. Ainsi, un seul script PHP est exposé en HTTP, ce qui limite les problèmes de duplicate content et améliore la sécurité du site. C'est le front controller qui répartit (dispatch) le travail aux autres contrôleurs.

Puisque les URLs d'un site utilisant une architecture MVC sont fictives, cela nous impose d'utiliser une forme d'URL Rewriting. Ainsi, les URLs peuvent rester identiques à celles définies depuis nos essais d'URL Rewriting. Tout dépend de notre découpage modules/actions.

Notre site actuel est trop simple pour être un exemple suffisamment explicite, mais nous pouvons facilement dégager des actions au sein de chaque contrôleur.

Les contrôleurs :
  • Article : Permet de lister (et d'administrer) les articles ;
  • Auteur : Permet de lister (et d'administrer) les auteurs ;
  • Erreur : Dicte la conduite en cas d'erreur.
Actions dans le contrôleur "Auteur" :
  • index : Liste les acteurs ;
  • display : Affiche les articles d'un auteur ;
  • (filter : Dans un site plus complet, cette action permet d'avoir une liste filtrée) ;
  • (update : Dans un site plus complet, cette action met à jour un auteur) ;
  • (delete : Dans un site plus complet, cette action supprime un auteur) ;
  • etc.
Actions dans le contrôleur "Article" :
  • index : Liste les articles ;
  • display : Affiche un article ;
  • (filter : Dans un site plus complet, cette action permet d'avoir une liste filtrée) ;
  • (update : Dans un site plus complet, cette action met à jour un article) ;
  • (delete : Dans un site plus complet, cette action supprime un article) ;
  • etc.
Actions dans le contrôleur "Erreur" :
  • index : Serveur indisponible ;
  • http : Redirection, page inaccessible ;
  • (sql : Erreur interne) ;
  • etc.

Il est maintenant très clair que nous devrions avoir un contrôleur "Catégorie" et que, depuis le début de ce tutoriel, il manque un script pour afficher les catégories. Une bonne architecture peut aider à déceler les problèmes dans les applications.

Arborescence sous "C:\Web\online\http\cours-php\6-mvc" (DocumentRoot) :
  • /
  • /index.php
Arborescence sous "C:\Web\offline\sites\cours-php\6-mvc" (hors de DocumentRoot) :
  • /
  • /MyPDO.php
  • /Controller
  • /Controller/Article.php
  • /Controller/Author.php
  • /Controller/Error.php
  • /Controller/Index.php
  • /Controller/Template.php
  • /Model
  • /Model/Article.php
  • /Model/Author.php
  • /Model/Category.php
  • /Model/Template.php
  • /View
  • /View/footer.tpl
  • /View/header.tpl
  • /View/article/
  • /View/article/index.tpl
  • /View/article/display.tpl
  • /View/author/
  • /View/author/index.tpl
  • /View/author/display.tpl
  • /View/error/
  • /View/error/index.tpl
  • /View/index/
  • /View/index/index.tpl

VIII-G-2. Tous les scripts

Ajouter à "httpd.conf" :
Sélectionnez
<Directory "C:/Web/online/http/cours-php/6-mvc">
    AllowOverride None
    php_value include_path ".;C:/Web/offline/sites/cours-php/6-mvc"
    SetEnv HTTP_ROOT /cours-php/6-mvc/
    RewriteEngine on
    RewriteCond %{REQUEST_URI} !\.(js|css|jpg|png|gif)$
    RewriteRule .* index.php
</Directory>

La directive RewriteCond ci-dessus n'est pas utile dans notre exemple, mais elle vous montre comment on évite aux images ainsi qu'aux scripts CSS et Javascript etc. de passer par notre contrôleur frontal PHP. En effet, ce serait une très grande perte de ressources et cela n'aurait aucune valeur ajoutée.

index.php
Sélectionnez
<?php
require 'functions.php';

spl_autoload_register('generic_autoload');
Controller_Template::$db = new MyPDO(
    'mysql:host=localhost;dbname=developpez', 'utilisateur', 'motdepasse');

preg_match(
    '#^'.$_SERVER['HTTP_ROOT'].'(?:([a-z]+)/)?#',
    $_SERVER['REQUEST_URI'],
    $match);

if(empty($match[1]))
{
    $controller = Controller_Index::getInstance();
    $controller->index();
}
else
{
    switch($match[1])
    {
        case 'articles':
            $controller = Controller_Article::getInstance();
            $controller->index();
        break;

        case 'auteurs':
            $controller = Controller_Author::getInstance();
            $controller->index();
        break;

        case 'article':
            if(preg_match(
                '#^'.$_SERVER['HTTP_ROOT'].'article/([0-9]+)-.*$#',
                $_SERVER['REQUEST_URI'],
                $match))
            {
                $controller = Controller_Article::getInstance();
                $controller->display($match[1]);
            }
            else
            {
                Controller_Error::documentNotFound(
                    "Page introuvable : URL incorrecte");
            }
        break;

        case 'auteur':
            if(preg_match(
                '#^'.$_SERVER['HTTP_ROOT'].'auteur/([0-9]+)-.*$#',
                $_SERVER['REQUEST_URI'],
                $match))
            {
                $controller = Controller_Author::getInstance();
                $controller->display($match[1]);
            }
            else
            {
                Controller_Error::documentNotFound(
                    "Page introuvable : URL incorrecte");
            }
        break;

        default:
            Controller_Error::documentNotFound(
                "Page introuvable : URL incorrecte");
    }
}
functions.php
Sélectionnez
<?php
function html($string)
{
    return utf8_encode(htmlspecialchars($string, ENT_QUOTES));
}

function url_format($string)
{
    $string = str_replace(' ', '-', strtolower($string));
    return html(preg_replace('#[^a-z0-9-]#', '', $string));
}

function generic_autoload($class)
{
    require_once str_replace('_', '/', $class).'.php';
}
MyPDO.php
Sélectionnez
<?php
class MyPDO extends PDO
{
    public function __construct($dsn, $user=NULL, $password=NULL)
    {
        parent::__construct($dsn, $user, $password);
        $this->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
    }

    public function prepare($sql, $options=NULL)
    {
        $statement = parent::prepare($sql);
        if(strpos(strtoupper($sql), 'SELECT') === 0) //requête "SELECT"
        {
            $statement->setFetchMode(PDO::FETCH_ASSOC);
        }

        return $statement;
    }
}
Controller/Article.php
Sélectionnez
<?php
class Controller_Article extends Controller_Template
{
    protected $categoriesModel;

    protected function __construct()
    {
        parent::__construct();
        $this->selfModel = new Model_Article();
        $this->categoriesModel = NULL;
    }

    public function index()
    {
        $title = "Liste des publications";
        $this->categoriesModel = new Model_Category();
        $categories = $this->categoriesModel->getAll();

        foreach($categories as $i => $category)
        {
            $categories[$i]['articles'] = $this->selfModel->getByCategory(
                $category['id']);
        }

        header('Content-Type: text/html; charset=utf-8');
        require 'View/header.tpl';
        require 'View/article/index.tpl';
        require 'View/footer.tpl';
    }

    public function display($id)
    {
        $article = $this->selfModel->getById($id);
        if(!$article)
        {
            Controller_Error::documentNotFound();
        }
        else
        {
            $title = $article['title'];

            header('Content-Type: text/html; charset=utf-8');
            require 'View/header.tpl';
            require 'View/article/display.tpl';
            require 'View/footer.tpl';
        }
    }
}
Controller/Author.php
Sélectionnez
<?php
class Controller_Author extends Controller_Template
{
    protected $articlesModel;

    protected function __construct()
    {
        parent::__construct();
        $this->selfModel = new Model_Author();
        $this->articlesModel = NULL;
    }

    public function index()
    {
        $title = "Liste des auteurs";
        $authors = $this->selfModel->getAll();

        header('Content-Type: text/html; charset=utf-8');
        require 'View/header.tpl';
        require 'View/author/index.tpl';
        require 'View/footer.tpl';
    }

    public function display($id)
    {
        $author = $this->selfModel->getById($id);
        if(!$author)
        {
            Controller_Error::documentNotFound("Cet auteur n'existe pas");
        }
        else
        {
            $title = "Publications de ".$author['name'];
            $this->articlesModel = new Model_Article();
            $articles = $this->articlesModel->getByAuthor($id);

            header('Content-Type: text/html; charset=utf-8');
            require 'View/header.tpl';
            require 'View/author/display.tpl';
            require 'View/footer.tpl';
        }
    }
}
Controller/Error.php
Sélectionnez
<?php
abstract class Controller_Error extends Controller_Template
{
    public static function documentNotFound($title)
    {
        header('HTTP/1.1 404 Not Found');
        header('Content-Type: text/html; charset=utf-8');
        include 'View/header.tpl';
        include 'View/error/404.tpl';
        include 'View/footer.tpl';
    }
}
Controller/Index.php
Sélectionnez
<?php
class Controller_Index extends Controller_Template
{
    public function index()
    {
        $title = "Guillaume Rossolini - Tutoriels PHP";

        header('Content-Type: text/html; charset=utf-8');
        require 'View/header.tpl';
        require 'View/index/index.tpl';
        require 'View/footer.tpl';
    }
}
Controller/Template.php
Sélectionnez
<?php
abstract class Controller_Template
{
    protected $selfModel;
    protected static $instance;
    public static $db;

    protected function __construct()
    {
    }

    public static function getInstance()
    {
        $class = get_called_class();
        if(!$class::$instance)
        {
            $class::$instance = new $class();
            return $class::$instance;
        }
    }
}

L'utilisation de la fonction get_called_class() nous oblige à utiliser au minimum la version 5.3 de PHP.

Model/Article.php
Sélectionnez
<?php
class Model_Article extends Model_Template
{
    protected $selectById;
    protected $selectByAuthor;
    protected $selectByCategory;

    public function __construct()
    {
        parent::__construct();

        $sql = 'SELECT ar.title, ar.text,
                    au.id AS author_id, au.name AS author_name
                FROM article AS ar
                INNER JOIN author AS au ON ar.author_id = au.id
                WHERE ar.id = ?';
        $this->selectById = Controller_Template::$db->prepare($sql);

        $sql = 'SELECT id, title
                FROM article
                WHERE author_id = ?
                ORDER BY title';
        $this->selectByAuthor = Controller_Template::$db->prepare($sql);

        $sql = 'SELECT ar.id, ar.title,
                    au.id AS author_id, au.name AS author_name
                FROM article AS ar
                INNER JOIN author AS au ON ar.author_id = au.id
                WHERE ar.category_id = ?
                ORDER BY ar.title';
        $this->selectByCategory = Controller_Template::$db->prepare($sql);
    }

    public function getById($id)
    {
        $this->selectById->execute(array($id));
        $articles = $this->selectById->fetchAll();
        if(empty($articles[0]))
        {
            return array();
        }
        else
        {
            $articles[0]['text'] = utf8_decode($articles[0]['text']);
            return $articles[0];
        }
    }

    public function getByAuthor($id)
    {
        $this->selectByAuthor->execute(array($id));
        return $this->selectByAuthor->fetchAll();
    }

    public function getByCategory($id)
    {
        $this->selectByCategory->execute(array($id));
        return $this->selectByCategory->fetchAll();
    }
}
Model/Author.php
Sélectionnez
<?php
class Model_Author extends Model_Template
{
    public function __construct()
    {
        parent::__construct();

        $sql = 'SELECT au.id, au.name, COUNT(*) AS nb_articles
                FROM author AS au
                LEFT JOIN article AS ar ON ar.author_id = au.id
                GROUP BY au.id, au.name
                ORDER BY name';
        $this->selectAll = Controller_Template::$db->prepare($sql);

        $sql = 'SELECT name
                FROM author
                WHERE id = ?';
        $this->selectById = Controller_Template::$db->prepare($sql);
    }
}
Model/Category.php
Sélectionnez
<?php
class Model_Category extends Model_Template
{
    public function __construct()
    {
        parent::__construct();

        $sql = 'SELECT c.id, c.name, COUNT(*) AS nb_articles
                FROM category AS c
                INNER JOIN article AS a ON a.category_id = c.id
                GROUP BY c.id, c.name
                ORDER BY c.name';
        $this->selectAll = Controller_Template::$db->prepare($sql);
    }
}
Model/Template.php
Sélectionnez
<?php
abstract class Model_Template
{
    protected $selectAll;
    protected $selectById;

    public function __construct()
    {
    }

    public function getAll()
    {
        $this->selectAll->execute();
        return $this->selectAll->fetchAll();
    }

    public function getById($id)
    {
        $this->selectById->execute(array($id));
        $row = $this->selectById->fetchAll();
        if(empty($row[0]))
        {
            return array();
        }
        else
        {
            return $row[0];
        }
    }
}
View/header.tpl
Sélectionnez
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="fr-FR" lang="fr-FR">
<head>
    <title><?php echo html($title); ?></title>
    <meta http-equiv="content-type" content="text/html; charset=utf-8" />
<style type="text/css">
h1 {
    text-align: center;
}
</style>
</head>
<body>
<ul style="text-align: center">
    <li style="display: inline"><a
        href="<?php echo html($_SERVER['HTTP_ROOT']); ?>"
        title="G. Rossolini - Tutoriels PHP">Accueil</a> -</li>
    <li style="display: inline"><a
        href="<?php echo html($_SERVER['HTTP_ROOT']); ?>articles/"
        title="G. Rossolini - Liste des publications">Publications</a> -</li>
    <li style="display: inline"><a
        href="<?php echo html($_SERVER['HTTP_ROOT']); ?>auteurs/"
        title="G. Rossolini - Liste des auteurs">Auteurs</a></li>
</ul>
<h1><?php echo html($title); ?></h1>
View/footer.tpl
Sélectionnez

<p style="text-align: center; font-size: 0.7em;">Copyright
    Guillaume Rossolini 2007-<?php echo date('Y'); ?><br />
Contact : <a
    href="mailto:g-rossolini@developpez.com"
    title="E-mail">g-rossolini@developpez.com</a></p>
</body>
</html>
View/article/display.tpl
Sélectionnez
<div style="text-align: center; font-style: italic">Par <a
    href="<?php echo html($_SERVER['HTTP_ROOT']); ?>auteur/<?php
        echo url_format($article['author_id'].'-'.$article['author_name']);
    ?>" title="Publications de <?php
        echo html($article['author_name']);
    ?>"><?php echo html($article['author_name']); ?></a></div>
<?php
echo nl2br(html($article['text']));
View/article/index.tpl
Sélectionnez
<?php foreach($categories as $category) : ?>
<h2><?php echo html($category['name']); ?></h2>
<ul>
    <?php foreach($category['articles'] as $article) : ?>
    <li><a href="<?php echo html($_SERVER['HTTP_ROOT']); ?>article/<?php
        echo url_format($article['id'].'-'.$article['title'])
    ?>" title="<?php echo html($article['title']); ?>"><?php
        echo html($article['title']);
    ?></a>, par <a href="<?php
        echo html($_SERVER['HTTP_ROOT']);
    ?>auteur/<?php
        echo url_format($article['author_id'].'-'.$article['author_name']);
    ?>" title=""><?php echo html($article['author_name']); ?></a></li>
    <?php endforeach; ?>
</ul>
<?php endforeach; ?>
View/author/display.tpl
Sélectionnez
<ul>
    <?php foreach($articles as $article) : ?>
    <li><a href="<?php echo html($_SERVER['HTTP_ROOT']); ?>article/<?php
        echo url_format($article['id'].'-'.$article['title']);
    ?>" title="Article par <?php echo html($author['name']); ?>"><?php
        echo html($article['title']);
    ?></a></li>
    <?php endforeach; ?>
</ul>
View/author/index.tpl
Sélectionnez
<ul style="list-style-type: none">
    <?php foreach($authors as $author) : ?>
    <li><a href="<?php echo html($_SERVER['HTTP_ROOT']); ?>auteur/<?php
        echo url_format($author['id'].'-'.$author['name']);
    ?>" title="<?php echo html($author['name']); ?>"><?php
        echo html($author['name']);
    ?></a> (<?php echo (int)$author['nb_articles']; ?> articles)</li>
    <?php endforeach; ?>
</ul>
View/error/404.tpl
Sélectionnez
Le document <?php echo html($_SERVER['REQUEST_URI']); ?> n'existe pas.
View/index/index.tpl
Sélectionnez
<h2 style="text-decoration: underline">Présentation</h2>
<p>J'ai suivi une formation dans plusieurs domaines, notamment la communication,
    le développement de logiciels, la gestion de projet,
    <span style="font-style: italic">etc</span>.</p>
<p>Aujoud'hui, je suis un développeur PHP muni de la certification Zend :<br />
<a href="http://www.zend.com/store/education/certification/authenticate.php?ClientCandidateID=ZEND005053&RegistrationID=221302133"
    title="Zend Certified Engineer Details"><img
        src="http://www.zend.com/images/training/php5_zce_logo.gif"
        alt="Logo ZCE"
        style="border: none"/></a></p>

VIII-G-3. Conclusion

Cette architecture est largement plus robuste et flexible que celle du premier exemple, mais elle n'est pas encore tout à fait parfaite. MVC a tendance à pousser à l'utilisation de nombreux Design patterns, notamment Singleton et Registry. Je n'ai pas mis en place un système trop complexe car notre petit site d'exemple ne le justifie pas, mais par exemple la connexion à la BDD mériterait un pattern Registry plutôt que d'être mise dans un contrôleur ou dans un modèle, alors que ni l'un ni l'autre ne sont prévus pour cela.

Par ailleurs, la simplicité fonctionnelle du site actuel ne rend pas justice au front controller. Je vous recommande d'étudier le fonctionnement de frameworks MVC et de juger par vous-mêmes de l'approche la plus adaptée à vos projets.

Ne vous fiez pas à la taille des scripts présentés ici pour juger de l'utilité de ce que je viens de vous montrer. Ce site d'exemple est bien trop petit pour que MVC soit utilisé correctement. Pour mieux comprendre l'intérêt du pattern MVC, ajoutez des fonctionnalités à ce site dans chacune des approches vues ici. C'est un très bon exercice de style...


précédentsommairesuivant

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

  

Copyright © 2008 Guillaume Rossolini. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.