Réalisation d’un site web avec PHP, MySql et jQuery Mobile
Librement traduit de l’article original http://tutorialzine.com/2011/08/jquery-mobile-product-website/

Dans ce tutorial, nous allons réaliser un petit site avec PHP et MySQL, en utilisant la bonne pratique Model-Vue-Controleur (MVC).
Puis, en utilisant le framework jQuery Mobile nous allons le transformer en un site optimisé pour les appareils nomades, qui fonctionnera sur différents mobiles et tailles écrans.

Dans la première partie nous concentrerons nos efforts sur la programmation coté serveur, l’élaboration de la base de données et l’organisation des sources dans le cadre MVC.
Dan la deuxième partie nous écrirons les vues et intégrerons jQuery Mobile.

ORGANISATION DES FICHIERS

Comme nous avons choisi de respecter le modèle MVC (en écrivant un mini framework maison), nous devons créer une arborescence de répertoires pour les modèles, les vues et les controleurs.
Ne soyez pas effrayé par le grand nombre de fichiers, en réalité il y a peu de code et celui-ci est très simple.


Schéma de la base de données

L’application utilisera 2 type d’entités : des catégories et des produits.
Ce qui donne lieu à 2 tables physiques : jqm_categories et jqm_products.
Chaque produit est rattaché par une clef étrangère à une catégorie.

 jqm_categories

Le champs ‘contains’ indique combien de produits il y a par catégorie (pas très optimisé niveau sgbd mais évite une requête supplémentaire –Note du traducteur).

jqm_products

Le champs  ‘catégory’ est la clef étrangère qui pointe vers l’id de la table ‘jqm_categories’

Après avoir créé ses tables et importé les données (le code SQL est fourni) il faut juste renseigner correctement le fichier ‘config.php’.

Voici le fichier config.php qui donne les infos pour la connexion et permet de configurer différentes variables.

<?php

// include/config.php

error_reporting(E_ALL ^ E_NOTICE);

 

/*=========== Base de données ==========*/

 

$db_host = 'localhost';

$db_user = 'root';

$db_pass = '';

$db_name = 'mobilestore';

 

 

/*=========== Divers configuration ==========*/

 

$defaultTitle = 'Vente de mobile';

$defaultFooter = date('Y');

 

?>
LES MODELES

Les modèles dans notre application vont se charger de communiquer avec la base de données.
Nous avons 2 types de ressources ; produits et catégories. La couche modèle va nous permettre d’extraire facilement les informations de ces ressources avec une méthode ‘find()’ qui va travailler au corps la base de données pour nous ramener les informations avec un tableau d’objets.

Avant de travailler avec la couche modèle, il faut s’assurer de pouvoir travailler avec la base de données. Ce travail est effectué par un include appelé ‘connect.php’ qui utilise la classe PHP PDO.

<?php //  includes/connect.php

try {

    $db = new PDO(

        "mysql:host=$db_host;dbname=$db_name;charset=UTF-8",

        $db_user,

        $db_pass

    );

 

    $db->query("SET NAMES 'utf8'");

    $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

}

catch(PDOException $e) {

    error_log($e->getMessage());

    die("Erreur de base de donnée");

}

 

L’include ‘connect.php’ sera appelé très tôt dans l’applicatif ce qui fait que les modèles pourront utiliser la connection sans problème.
Chaque fichier de la couche modèle contient une définition de classe qui expose une seule méthode statique ‘find()’ ( statique, donc accessible sans créer d’instance).

<?php // includes/models/category.model.php
class Category{
public static function find($arr = array()){
        global $db;
        if(empty($arr)){
            $st = $db->prepare("SELECT * FROM jqm_categories");
        }
        else if($arr['id']){
           $st = $db->prepare("SELECT * FROM jqm_categories WHERE id=:id");     }        else{
           throw new Exception("Impossible de traiter ce cas!");
        }
            try
            { $st->execute($arr);}
            catch(PDOException $e)
           
{/*var_dump($e); */     echo $e->errorInfo[2];  }
        return $st->fetchAll(PDO::FETCH_CLASS, "Category");
    }
}
?>


En fonction des arguments qu’elle reçoit (rien ou un id) la méthode va renvoyer différents résultats.
Soit la liste de toutes les catégories (pas d’argument passé) soit une catégorie particulière.

Même principe pour la classe Product :

<?php // includes/models/product.model.php
class
Product{
 
    public static function find($arr){
        global $db;
 
        if($arr['id']){
            $st = $db->prepare("SELECT * FROM jqm_products WHERE id=:id");
        }
        else if($arr['category']){
            $st = $db->prepare("SELECT * FROM jqm_products WHERE category = :category");
        }
        else{
            throw new Exception("Argument inconnu!");
        }
 
        $st->execute($arr);
 
        return $st->fetchAll(PDO::FETCH_CLASS, "Product");
    }
}

Si la méthode (ou fonction) ‘find’ reçoit un id  elle va renvoyer le tuple  du produit concerné par cet id, si la méthode ‘find’ reçoit une catégorie elle va renvoyer les tuples des produits concernés par cette catégorie.

Maintenant que les modèles sont codés, penchons nous sur la couche controleur.


LES CONTROLEURS

Les scripts controleurs utilisent la méthode find de la couche modèle pour rapatrier les données et informer les bonnes vues. Nous avons deux controleurs dans cette application : Un pour la page d’accueil et un autre pour la page des catégories.

<?php // includes/controllers/home.controller.php

class HomeController{
    public function handleRequest(){
 
        $content = Category::find();
 
        render('home',array(
            'title'    => 'Bienvenue dans notre magasin',
            'content'  => $content
        ));
    }
}

 

Chaque controleur est équipé d’une methode handleRequest() . Cette méthode est appelée pour chaque demande d’url. Nous en reparlerons un petit peu plus loin avec le script ‘index.php’.
Ce controleur rapporte toutes les catégories (merci au modele) et utilise une fonction pratique pour le rendu en transmettant différentes informations.
C’est « un poil » plus complexe avec le controleur dédié aux catégories :

<?php // includes/controllers/category.controller.php
class
CategoryController{
    public function handleRequest(){
        $cat = Category::find(array('id'=>$_GET['category']));
 
        if(empty($cat)){
            throw new Exception("There is no such category!");
        }
 
        $categories = Category::find();
 
        $products = Product::find(array('category'=>$_GET['category']));
 
 
        render('category',array(
            'title'                   => $cat[0]->name,
            'categories'       => $categories,
            'products'         => $products
        ));
    }
}

La première chose que fait ce controleur est de rechercher une catégorie grâce à son id (qui est passé par l’url). Si tout est ok il ramène la liste de toutes les catégories et la liste des produits de la première (ramenée via l’id).

Maintenant, examinons le fichier index.php afin de voir comment tous ces scripts fonctionnent ensemble.

<?php // index.php
require_once
"includes/main.php";
 
try {
 
    if($_GET['category']){
        $c = new CategoryController();
    }
    else if(empty($_GET)){
        $c = new HomeController();
    }
    else throw new Exception('Mauvaise page!');
 
    $c->handleRequest();
}
catch(Exception $e) {
    // Affichage d’erreur via une function ‘helper’
    render('error',array('message'=>$e->getMessage()));
}
?>

C’est le fichier appelé à chaque requête.
Il examine les arguments passés en GET (tableau associatif $_GET), crée un objet controleur et appelle la méthode handleRequest().
Si il y a une erreur, une exception est levée et sera traitée comme il se doit avec l’affichage qui va bien.

Attention, il faut charger tous les includes sous peine d’avoir l’erreur suivante :


C’est le rôle du fichier main.php :

require_once "includes/config.php";
require_once "includes/connect.php";
require_once "includes/helpers.php";
require_once "includes/models/product.model.php";
require_once "includes/models/category.model.php";
require_once "includes/controllers/home.controller.php";
require_once "includes/controllers/category.controller.php";
 
// Demande au navigateur de mettre les pages en cache.
header('Cache-Control: max-age=3600, public');
header('Pragma: cache');
header("Last-Modified: ".gmdate("D, d M Y H:i:s",time())." GMT");
header("Expires: ".gmdate("D, d M Y H:i:s",time()+3600)." GMT");

 

 


Passons maintenant à l’habillage avec jQuery Mobile
Et c’est donc tout naturellement que nous allons aborder LES VUES

Les vues seront des fichiers PHP ou des templates qui génèrent du code HTML.
Elles sont appelées par les controleurs qui utilisent la fonction (ou helper) ‘render()’.

Nous avons 6 vues pour ce site :
_category.php, _product.php, _header.php, _footer.php, category.php, et home.php.

Nous les verrons en détail, mais pour l’instant, penchons nous sur cette fameuse fonction ‘render()’.

<?php // include/helpers.php
function render($template,$vars = array()){
 
    // Cette function prend en argument un nom de template
    // avec une liste de variables et affiche le tout.
 
    // Variables créées via le tableau
    extract($vars);
 
    // Si on trouve un tableau
    if(is_array($template)){
 
        // Alors c’est un cas particulier à traiter 
        // dans une vue partielle
        foreach($template as $k){
 
            // Création à la volée d’un objet à partir 
            // du nom de la classe récupéré
            $cl = strtolower(get_class($k));
            $$cl = $k;
 
            include "views/_$cl.php";
        }
 
    }
    else {
        include "views/$template.php";
    }
}

 

Le premier argument de la function contient le nom du fichier template qui se trouve dans le répertoire des vues. Le deuxième est un tableau qui contient plusieurs arguments. Ce tableau est « extrait » (fonction extract() de PHP) en variables qui pourront être utilisées dans le template.

Le principe est de retrouver le nom de la classe : Catégorie ou Produit ?
Puis on instancie l’objet correspondant pour en informer l’affichage.
Concrètement voila ce qui est passé dans le tableau $template :

Si il s’agit de l’affichage d’une catégorie
Array(   
[0] => Category Object       (           [id] => 1            [name] => Notebooks            [contains] => 3        )

    [1] => Category Object        (          [id] => 2            [name] => Smartphones            [contains] => 4        )
    [2] => Category Object
        (            [id] => 3            [name] => Tablets            [contains] => 4        )

)
Si il s’agit de l’affichage d’un produit
[0] => Product Object        (           
[id] => 1    [category] => 1    [name] => MacBook Air  [manufacturer] => Apple   [price] => 999        )
 [1] => Product Object        (
 [id] => 2     [category] => 1  [name] => MacBook Pro     [manufacturer] => Apple   [price] => 1500    )
[2] => Product Object        (
[id] => 3  [category] => 1  [name] => Vaio   [manufacturer] => Sony         [price] => 899        )

 


LES VUES

Commençons par la première ; le header.
Cette vue reprend les différents éléments qui doivent figurer dans une page HTML5, les liens vers les scripts coté client (jquery,css …) et un petit squelette. La seule variable qu’elle afficher est la variable $title.

includes/views/_header.php

<!DOCTYPE html>
<html>
    <head>
    <title><?php echo $title; ?></title> 
 
    <meta name="viewport" content="width=device-width, initial-scale=1" /> 
 
    <link rel="stylesheet" href="http://code.jquery.com/mobile/1.0b2/jquery.mobile-1.0b2.min.css" />
    <link rel="stylesheet" href="assets/css/styles.css" />
    <script type="text/javascript" src="http://code.jquery.com/jquery-1.6.2.min.js"></script>
    <script type="text/javascript" src="http://code.jquery.com/mobile/1.0b2/jquery.mobile-1.0b2.min.js"></script>
</head>
<body> 
 
<div data-role="page">
 
    <div data-role="header" data-theme="b">
        <a href="./" data-icon="home" data-iconpos="notext" data-transition="fade">Home</a>
        <h1><?php echo $title?></h1>
    </div>
 
    <div data-role="content">

 

Le contenu du squelette contient des div avec des attributs particuliers (data-role), compris par le framework javascript jQuery Mobile, afin de modifier l’affichage.
Pour en savoir plus le mieux est d’aller consulter la documentation sur le site du framework : http://jquerymobile.com/demos/1.0b2/#/demos/1.0b2/docs/pages/page-anatomy.html (ou chercher avec Google : jquerymobile Anatomy of a Page)

Dans le code précédent, on voit bien qu’une balise DIV n’est pas fermée.
En effet, celle-ci est fermée dans la vue includes/views/_footer.php

</div>
 
    <div data-role="footer" id="pageFooter">
        <h4><?php echo $GLOBALS['defaultFooter']?></h4>
    </div>
</div>
 
</body>
</html>

Rien de spécial ici. Juste une variable globale -'defaultFooter'- positionnée via includes/config.php

Après ces vues purement ‘techniques’ (header et footer) nous codons maintenant des vues plus ‘fonctionnelles’ qui ont vocation à afficher du contenu qui a du sens du point de vue de l’utilisateur.

includes/views/home.php

<?php render('_header',array('title'=>$title))?>
 
<ul data-role="listview" data-inset="true" data-theme="c" data-dividertheme="b">
    <li data-role="list-divider">Choisir une categorie</li>
    <?php render($content) ?>
</ul>
 
<?php render('_footer')?>

 

includes/views/category.php

<?php render('_header',array('title'=>$title))?>
 
<div class="rightColumn">
    <ul data-role="listview" data-inset="true" data-theme="c" data-dividertheme="c">
        <?php render($products) ?>
    </ul>
</div>
 
<div class="leftColumn">
    <ul data-role="listview" data-inset="true" data-theme="c" data-dividertheme="b">
        <li data-role="list-divider">Categories</li>
        <?php render($categories,array('active'=>$_GET['category'])) ?>
    </ul>
</div>
 
<?php render('_footer')?>


includes/views/_product.php

<li class="product">
    <img src="assets/img/<?php echo $product->id ?>.jpg" alt="<?php echo $product->name ?>" />
    <?php echo $product->name ?> <i><?php echo $product->manufacturer?></i>
    <b>$<?php echo $product->price?></b>
</li>

Lorsqu’un fichier commence par un underscore (‘_’) c’est pour indiquer, à la mode Ruby on Rail, que celui-ci va être inclus dans un autre. On parle également de classes ‘partielles’.
includes/views/_category.php
<li <?php echo ($active == $category->id ? 'data-theme="a"' : '') ?>>
<a href="?category=<?php echo $category->id?>" data-transition="fade">
               <?php echo $category->name ?>
    <span class="ui-li-count"><?php echo $category->contains?></span></a>
</li>

C’est terminé pour la partie PHP.
Voici la feuille de style assets/css/styles.css
 
 media all and (min-width: 650px){
    .rightColumn{
        width:56%;
        float:right;
        margin-left:4%;
    }
    .leftColumn{
        width:40%;
        float:left;
    }
}
.product i{
    display:block;
    font-size:0.8em;
    font-weight:normal;
    font-style:normal;
}
.product img{
    margin:10px;
}
.product b{
    position: absolute;
    right: 15px;
    top: 15px;
    font-size: 0.9em;
}
.product{
    height: 80px;
}

Le media query à 650 px indique que si l’écran est moins large alors les différent éléments vont glisser les une sous les autres.