Creación de un sitio web con PHP, MySQL y jQuery Mobile, Parte 1

En este tutorial de dos partes, construiremos un sitio web simple con PHP y MySQL, utilizando el patrón Model-View-Controller (MVC). Finalmente, con la ayuda del marco jQuery Mobile, lo convertiremos en un sitio web móvil compatible con pantallas táctiles que funcione en cualquier dispositivo y tamaño de pantalla.

En esta primera parte, nos concentramos en el backend, discutiendo la base de datos y la organización de MVC. En la segunda parte, estamos escribiendo las vistas e integrando jQuery Mobile.

La estructura del archivo

Como estaremos implementando el patrón MVC (en efecto, escribiendo un micro-marco simple), es natural dividir la estructura de nuestro sitio en diferentes carpetas para los modelos, vistas y controladores. No deje que la cantidad de archivos lo asuste; aunque estamos usando muchos archivos, el código es conciso y fácil de seguir.

El esquema de la base de datos

Nuestra sencilla aplicación funciona con dos tipos de recursos:categorías y productos. Estos tienen sus propias tablas:jqm_categories y jqm_products . Cada producto tiene un campo de categoría, que lo asigna a una categoría.

La tabla de categorías tiene un ID campo, un nombre y un contiene columna, que muestra cuántos productos hay en cada categoría.

La tabla de productos tiene un nombre , fabricante , precio y una categoría campo. Este último contiene el ID de la categoría a la que se agrega el producto.

Los modelos

Los modelos en nuestra aplicación manejarán la comunicación con la base de datos. Tenemos dos tipos de recursos en nuestra aplicación:productos y categorías . Los modelos expondrán un método fácil de usar:find() que consultará la base de datos detrás de escena y devolverá una matriz con objetos.

Antes de comenzar a trabajar en los modelos, necesitaremos establecer una conexión a la base de datos. Estoy usando la clase PHP PDO, lo que significa que sería fácil usar una base de datos diferente a MySQL, si es necesario.

incluye/conectar.php

/*
    This file creates a new MySQL connection using the PDO class.
    The login details are taken from includes/config.php.
*/

try {
    $db = new PDO(
        "mysql:host=$db_host;dbname=$db_name;charset=UTF8",
        $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("A database error was encountered");
}

Esto pondrá el $db objeto de conexión en el ámbito global, que usaremos en nuestros modelos. Puedes verlos a continuación.

incluye/modelos/categoría.modelo.php

class Category{

    /*
        The find static method selects categories
        from the database and returns them as
        an array of Category objects.
    */

    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("Unsupported property!");
        }

                // This will execute the query, binding the $arr values as query parameters
        $st->execute($arr);

        // Returns an array of Category objects:
        return $st->fetchAll(PDO::FETCH_CLASS, "Category");
    }
}

Ambos modelos son definiciones de clases simples con un solo método estático:find() . En el fragmento anterior, este método toma una matriz opcional como parámetro y ejecuta diferentes consultas como declaraciones preparadas.

En la declaración de retorno, estamos usando el método fetchAll pasándole el PDO::FETCH_CLASS constante. Lo que esto hace es recorrer todas las filas de resultados y crear un nuevo objeto de la clase Categoría. Las columnas de cada fila se agregarán como propiedades públicas al objeto.

Este es también el caso con el Modelo de producto :

incluye/modelos/producto.modelo.php

class Product{

    // The find static method returns an array
    // with Product objects from the database.

    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("Unsupported property!");
        }

        $st->execute($arr);

        return $st->fetchAll(PDO::FETCH_CLASS, "Product");
    }
}

Los valores de retorno de ambos métodos de búsqueda son matrices con instancias de la clase. Posiblemente podríamos devolver una matriz de objetos genéricos (o una matriz de matrices) en el método de búsqueda, pero la creación de instancias específicas nos permitirá diseñar automáticamente cada objeto usando la plantilla adecuada en la carpeta de vistas (las que comienzan con un guión bajo) . Volveremos a hablar de esto en la siguiente parte del tutorial.

Listo, ahora que tenemos nuestros dos modelos, sigamos con los controladores.

Los controladores

Los controladores usan find() métodos de los modelos para obtener datos y representar las vistas apropiadas. Tenemos dos controladores en nuestra aplicación:uno para la página de inicio , y otro para las páginas de categoría .

incluye/controladores/home.controller.php

/* This controller renders the home page */

class HomeController{
    public function handleRequest(){

        // Select all the categories:
        $content = Category::find();

        render('home',array(
            'title'     => 'Welcome to our computer store',
            'content'   => $content
        ));
    }
}

Cada controlador define un handleRequest() método. Este método se llama cuando se visita una URL específica. Volveremos a esto en un segundo, cuando discutamos index.php .

En el caso del HomeController , manejarSolicitud() simplemente selecciona todas las categorías utilizando el método find() del modelo y presenta la vista de inicio (includes/views/home.php ) usando nuestro render función auxiliar (incluye/helpers.php ), pasando un título y las categorías seleccionadas. Las cosas son un poco más complejas en CategoryController :

incluye/controladores/categoría.controlador.php

/* This controller renders the category pages */

class CategoryController{
    public function handleRequest(){
        $cat = Category::find(array('id'=>$_GET['category']));

        if(empty($cat)){
            throw new Exception("There is no such category!");
        }

        // Fetch all the categories:
        $categories = Category::find();

        // Fetch all the products in this category:
        $products = Product::find(array('category'=>$_GET['category']));

        // $categories and $products are both arrays with objects

        render('category',array(
            'title'         => 'Browsing '.$cat[0]->name,
            'categories'    => $categories,
            'products'      => $products
        ));
    }
}

Lo primero que hace este controlador es seleccionar la categoría por id (se pasa como parte de la URL). Si todo va según lo planeado, obtiene una lista de categorías y una lista de productos asociados con el actual. Finalmente, se representa la vista de categoría.

Ahora veamos cómo funcionan todos juntos, inspeccionando index.php :

index.php

/*
    This is the index file of our simple website.
    It routes requests to the appropriate controllers
*/

require_once "includes/main.php";

try {

    if($_GET['category']){
        $c = new CategoryController();
    }
    else if(empty($_GET)){
        $c = new HomeController();
    }
    else throw new Exception('Wrong page!');

    $c->handleRequest();
}
catch(Exception $e) {
    // Display the error page using the "render()" helper function:
    render('error',array('message'=>$e->getMessage()));
}

Este es el primer archivo que se llama en una nueva solicitud. Dependiendo del $_GET parámetros, crea un nuevo objeto controlador y ejecuta su handleRequest() método. Si algo sale mal en alguna parte de la aplicación, se generará una excepción que llegará a la cláusula catch y luego a la plantilla de error.

Una cosa más que vale la pena señalar es la primera línea de este archivo, donde requerimos main.php . Puedes verlo a continuación:

principal.php

/*
    This is the main include file.
    It is only used in index.php and keeps it much cleaner.
*/

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

// This will allow the browser to cache the pages of the store.

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

Este archivo contiene el require_once declaraciones para todos los modelos, controladores y archivos auxiliares. También define algunos encabezados para habilitar el almacenamiento en caché en el navegador (PHP deshabilita el almacenamiento en caché de forma predeterminada), lo que acelera el rendimiento del marco móvil jQuery.

Continuar a la Parte 2

¡Con esto la primera parte del tutorial está completa! Continúe con la parte 2, donde escribiremos las vistas e incorporaremos jQuery Mobile. Siéntase libre de compartir sus pensamientos y sugerencias en la sección de comentarios a continuación.