¡Hola! Me parece que andas usando un bloqueador de anuncios =( ¿Nos desbloqueas? ¿Por qué?
F13

Funcion 13

Localizador de tiendas con PHP, jQuery Mobile y MySQL – Parte 2

Esta es la segunda de tres partes de una serie de tutoriales sobre una aplicación completa usando PHP, jQuery Mobile, MySQL y Google Maps en el que podremos localizar establecimientos.

La idea es sencilla, tenemos varios establecimientos esparcidos por la geografía Española por lo que ofrecemos un listado de provincias, este carga un listado de poblaciones y finalmente ubicamos las tiendas disponibles en esa población o bien intentamos que nos localice tiendas cercanas a nuestra ubicación.

Dado que es una aplicación larga, vamos a dividirla por partes para no liarnos demasiado. En cada parte tocaremos un punto y seguiremos el orden que considero más lógico para crear una aplicación:

  1. Planear y crear la base de datos
  2. Desarrollar el backend con PHP que devolverá los datos a jQuery Mobile a través de JSON.
  3. Desarrollar la parte frontal con jQuery Mobile y Google Maps

Echa un vistazo a lo que vamos a desarrollar:

Después de haber creado los cimientos SQLs que aguantarán nuestra aplicación, en esta parte de la serie nos encargaremos de crear el código PHP que se encargará de comunicarse con la aplicación de jQuery Mobile.

La estructura de archivos será sencilla:

Un archivo para cada clase, nuestro controlador (que se encarga de recibir las peticiones) y un archivo DAO que contiene la propia lógica.

Como pronto descubriréis, todos los modelos heredan de la clase ORM que aprendimos a desarrollar y usar en este artículo.

Provincia

class Provincia extends ORM {  
    public $id, $nombre;
    protected static $table = 'provincias';
    public function __construct($data) {
        parent::__construct();
        if ($data && sizeof($data)) {
            $this->populateFromRow($data);
        }
    }
    public function populateFromRow($data) {
        $this->id = isset($data['id']) ? intval($data['id']) : null;
        $this->nombre = isset($data['nombre']) ? $data['nombre'] : null;
    }
}

Casi todas las clases se parecerán muchísimo a esta. Tenemos los valores del objeto, una función que rellena los datos y nuestro constructor.

Población

require_once ('Provincia.php');  
class Poblacion extends ORM {  
    public $id, $nombre, $provincia, $obj_provincia;
    protected static $table = 'poblaciones';
    public function __construct($data) {
        parent::__construct();
        if ($data && sizeof($data)) {
            $this->populateFromRow($data);
        }
    }
    public function populateFromRow($data) {
        $this->id = isset($data['id']) ? intval($data['id']) : null;
        $this->nombre = isset($data['nombre']) ? $data['nombre'] : null;
        $this->provincia = isset($data['provincia']) ? intval($data['provincia']) : null;
        if ($this->provincia) {
            $this->obj_provincia = Provincia::find($this->provincia);
        }
    }
}

Esta clase, tiene la particularidad, de que tiene una relación con la Provincia. Por tanto, si tenemos la id de la provincia, nos encargamos de obtenerla.

Tipo

class Tipo extends ORM {
    public $id, $nombre;
    protected static $table = 'tipos';
    public function __construct($data) {
        parent::__construct();
        if ($data && sizeof($data)) {
            $this->populateFromRow($data);
        }
    }
    public function populateFromRow($data) {
        $this->id = isset($data['id']) ? intval($data['id']) : null;
        $this->nombre = isset($data['nombre']) ? $data['nombre'] : null;
    }
}

Tienda

require_once ('Tipo.php');
require_once ('Poblacion.php');

class Tienda extends ORM {
    public $id, $sfid, $nombre_comercial, $tipo, $obj_tipo, $direccion, $cp, $poblacion, $obj_poblacion, $lat, $lng, $texto_promocion, $texto_animacion, $distancia;
    protected static $table = 'tiendas';
    public function __construct($data) {
        parent::__construct();

        if ($data && sizeof($data)) {
            $this->populateFromRow($data);
        }
    }
    public function populateFromRow($data) {
        $this->id = isset($data['id']) ? intval($data['id']) : null;
        $this->sfid = isset($data['nombre']) ? $data['nombre'] : null;
        $this->nombre_comercial = isset($data['nombre_comercial']) ? $data['nombre_comercial'] : null;
        $this->tipo = isset($data['tipo']) ? intval($data['tipo']) : null;

        if ($this->tipo && !isset($data['no_deep'])) {
            $this->obj_tipo = Tipo::find($this->tipo);
        }

        $this->direccion = isset($data['direccion']) ? $data['direccion'] : null;
        $this->cp = isset($data['cp']) ? $data['cp'] : null;
        $this->poblacion = isset($data['poblacion']) ? intval($data['poblacion']) : null;

        if ($this->poblacion && !isset($data['no_deep'])) {
            $this->obj_poblacion = Poblacion::find($this->poblacion);
        }

        $this->lat = isset($data['lat']) ? $data['lat'] : null;
        $this->lng = isset($data['lng']) ? $data['lng'] : null;
        $this->texto_promocion = isset($data['texto_promocion']) ? $data['texto_promocion'] : null;
        $this->texto_animacion = isset($data['texto_animacion']) ? $data['texto_animacion'] : null;
    }
}

Utils

class Utils {
    public static function getDistance($latitude1, $longitude1, $latitude2, $longitude2, $unit = 'Km') {
        $theta = $longitude1 - $longitude2;
        $distance = (sin(deg2rad($latitude1)) * sin(deg2rad($latitude2))) + (cos(deg2rad($latitude1)) * cos(deg2rad($latitude2)) * cos(deg2rad($theta)));
        $distance = acos($distance);
        $distance = rad2deg($distance);
        $distance = $distance * 60 * 1.1515;
        switch ($unit) {
            case 'Mi':
            break;
            case 'Km':
                $distance = $distance * 1.609344;
            break;
            case 'm':
                $distance = $distance * 1.609344 * 1000;
            break;
        }
        return $distance;
    }
}

En esta clase tiendo a guardar todas las funciones que son de Utilidad y por tanto, no tenemos necesidad de instanciar ninguna clase para hacer uso de las funciones. En este caso solo tenemos una función, getDistance, que usaremos más adelante.

Config

// Including Database Layer 
require_once(dirname(__FILE__) . '/inc/class/Database.php'); 

// Database 
define('DB_HOST', 'localhost');  
define('DB_USER', 'root');  
define('DB_PASSWORD', 'root');  
define('DB_DB', 'tiendas');  
define('DB_PROVIDER', 'MySqlProvider'); 

// Configuration 
define('DISTANCE_THRESHOLD', '5'); //Km  

Este es el archivo de configuración que expone algunas constantes que usaremos en nuestra aplicación como las credenciales a la base de datos, o el límite para la búsqueda por cercanía.

Controller

if (isset($_POST['accion']) || isset($_GET['accion'])) {  
    require_once ('config.php');
    require_once ('inc/class/Utils.php');
    require_once ('inc/class/ORM.php');
    require_once ('inc/class/DAO.php');
    require_once ('inc/class/Tienda.php');
    DAO::$database = Database::getConnection(DB_PROVIDER, DB_HOST, DB_USER, DB_PASSWORD, DB_DB);
    $action = isset($_POST['accion']) ? $_POST['accion'] : $_GET['accion'];
    $data = isset($_POST['accion']) ? $_POST : $_GET;
    date_default_timezone_set('Europe/Madrid');
    header('Content-Type: application/json');
    switch ($action) {
        case 'obtenerProvincias':
            echo json_encode(DAO::obtenerProvincias());
        break;
        case 'obtenerPoblaciones':
            echo json_encode(DAO::obtenerPoblacionesPorProvincia($data['provincia']));
        break;
        case 'obtenerTiendas':
            echo json_encode(DAO::obtenerTiendasPorPoblacion($data['poblacion']));
        break;
        case 'obtenerTiendasCercanas':
            echo json_encode(DAO::obtenerTiendasCercanas($data['provincia'], $data['poblacion'], $data['lat'], $data['lng']));
        break;
        default:
            echo json_encode(array('error' => true, 'mensaje' => 'Acción no implementada'));
        break;
    }
} else {
    header($_SERVER['SERVER_PROTOCOL'] . " 400 Bad Request", true, 400);
}

Este archivo podéis llamarlo como queráis, suelo llamarlo api aunque no sea del todo un API así que he decidido dejarlo como controller ahora mismo.

Es, sin duda, uno de los archivos con más miga. Lo primero que hacemos es ver si tenemos una petición POST o GET que venga con el parámetro acción. Si no es así, devolvemos un error 400, indicando que la petición que se ha hecho es errónea.

Incluimos las clases necesarias e inicializamos la conexión a la base de datos del DAO. Como ya vimos, esa conexión será la que usen todas las clases de ahora en adelante.

Guardamos los datos de la petición para no tener que preocuparnos más de si la petición es POST o GET, y establecemos la zona horaria por defecto y la cabecera que vamos a usar para devolver datos, que en este caso es JSON.

Si fueramos a usar JSONP la cabecera que usaríamos en PHP es:

header('Content-Type: application/javascript');  

Finalmente, tenemos todas las funciones que vamos a usar que veremos ahora en el archivo DAO. Solo añadir que, en caso de que hagamos una petición que no tengamos ahí, devolveremos un error.

DAO

class DAO {  
    public static $database;
    public static function obtenerProvincias() {
        return Provincia::all(' ORDER BY nombre');
    }
    public static function obtenerPoblacionesPorProvincia($provincia) {
        $results = Poblacion::where('provincia', $provincia);
        if ($results) {
            $poblaciones = null;
            foreach ($results as $index => $poblacion) {
                $poblaciones[] = array('id' => $poblacion->id, 'nombre' => $poblacion->nombre);
            }
            $result = $poblaciones;
        } else {
            $result = array('error' => true, 'mensaje' => 'No hay poblaciones para esa provincia');
        }
        return $result;
    }
    public static function obtenerTiendasPorPoblacion($poblacion) {
        $query = 'SELECT tiendas.id, nombre_comercial, tipos.nombre AS tipo, direccion, cp, poblaciones.nombre AS poblacion, lat, lng FROM tiendas INNER JOIN tipos ON tiendas.tipo = tipos.id INNER JOIN poblaciones ON tiendas.poblacion = poblaciones.id WHERE poblacion = ?';
        $results = self::$database->execute($query, null, array($poblacion));
        if ($results) {
            $result = $results;
        } else {
            $result = array('error' => true, 'mensaje' => 'No hay tiendas para esa poblacion');
        }
        return $result;
    }
    public static function obtenerTiendasCercanas($provincia, $poblacion, $lat, $lng) {
        $query = 'SELECT tiendas.id, nombre_comercial, tipos.nombre AS tipo, direccion, cp, poblaciones.nombre AS poblacion, lat, lng, provincias.nombre AS provincia FROM tiendas INNER JOIN tipos ON tiendas.tipo = tipos.id INNER JOIN poblaciones ON tiendas.poblacion = poblaciones.id INNER JOIN provincias ON poblaciones.provincia = provincias.id WHERE poblaciones.nombre = ?';
        $results = self::$database->execute($query, null, array($poblacion));
        if (!$results) { //Lo intentamos con la provincia
            $query = 'SELECT tiendas.id, nombre_comercial, tipos.nombre AS tipo, direccion, cp, poblaciones.nombre AS poblacion, provincias.nombre AS provincia, lat, lng FROM tiendas INNER JOIN tipos ON tiendas.tipo = tipos.id INNER JOIN poblaciones ON tiendas.poblacion = poblaciones.id INNER JOIN provincias ON poblaciones.provincia = provincias.id WHERE provincias.nombre = ?';
            $results = self::$database->execute($query, null, array($provincia));
        }
        if ($results) {
            $result = null;
            foreach ($results as $index => $store) {
                $distance = Utils::getDistance($lat, $lng, $store['lat'], $store['lng']);
                if ($distance <= DISTANCE_THRESHOLD) {
                    $store['distancia'] = round($distance, 2);
                    $result[] = $store;
                }
            }
            $distances = array();
            foreach ($result as $key => $store) {
                $distances[$key] = $store['distancia'];
            }
            array_multisort($distances, SORT_ASC, $result);
            if (!$result) {
                $result = array('error' => true, 'mensaje' => 'No hay tiendas cercanas en la ubicación actual');
            }
        } else {
            $result = array('error' => true, 'mensaje' => 'No hay tiendas cercanas en la ubicación actual');
        }
        return $result;
    }
}

Esta es la clase con más miga, ya que es la que realiza todas las operaciones que se llama desde el archivo controller.php.

En obtenerProvinciasobtenemos todas las provincias que tengamos cargadas en Base de Datos ya que nos servirá luego para cargarlo en un select.

La siguiente función, obtenerPoblacionesPorProvincia, nos servirá para obtener todas las poblaciones de una determinada provincia, esto es, cuando actualicemos el valor del desplegable de provincias, llamaremos esta función.

obtenerTiendasPorPoblacion nos saca las tiendas que hay en la población seleccionada.

Finalmente, obtenerTiendasCercanas se encarga de sacar las tiendas más cercanas. Lo que hacemos es sacar las tiendas que estén en una población. Si no encontramos ninguna, sacamos las que tengamos en la provincia. Luego, con la función de Utils, comprobamos que estén en el límite de DISTANCE_THRESHOLD, y las ordenamos por distancia.

Probando que todo funciona

Dado que no vamos a insertar datos, todas las peticiones son GET así que con nuestro navegador podemos hacer tantas pruebas como queramos.

He preparado un pequeño archivo SQL que contiene provincias, poblaciones, tipos y algunas tiendas de Apple. Las iba a cargar todas pero, era demasiado trabajo. Probad con Madrid y saldrán algunos resultados.

Si accedemos a esta URL:

http://localhost/localizador_tiendas/controller.php?accion=obtenerProvincias

Deberíamos obtener algo así:

[
   {
      "id":27,
      "nombre":"\u00c1lava"
   },
   {
      "id":12,
      "nombre":"Albacete"
   },
   {
      "id":20,
      "nombre":"Alicante"
   },
   {
      "id":49,
      "nombre":"Almer\u00eda"
   },
   {
      "id":31,
      "nombre":"Asturias"
   },
   {
      "id":9,
      "nombre":"\u00c1vila"
   },
   {
      "id":46,
      "nombre":"Badajoz"
   },
   {
      "id":22,
      "nombre":"Baleares"
   }
...

Mientras que si accedemos a esta:

http://localhost/localizador_tiendas/controller.php?accion=obtenerPoblaciones&provincia=1

Veremos todas las poblaciones que tenemos de Madrid:

[
   {
      "id":1,
      "nombre":"Madrid"
   },
   {
      "id":3,
      "nombre":"Alcorc\u00f3n"
   },
   {
      "id":6,
      "nombre":"Humanes"
   },
   {
      "id":7,
      "nombre":"Legan\u00e9s"
   },
   {
      "id":10,
      "nombre":"Fuenlabrada"
   },
   {
      "id":11,
      "nombre":"Collado Villalba"
   },
   {
      "id":12,
      "nombre":"Las Rozas"
   }
...

¡Genial! En la siguiente parte veremos cómo hacer la parte final, la parte de jQuery Mobile que une todas las piezas y nos muestra un resultado final.

Si el artículo te pareció interesante, útil o incluso equivocado, por favor considera el dejar un comentario. ¡Lo apreciaré mucho!

Programador Front-end en First + Third y Potato. Trabajando con JavaScript y HTML5 desde el corazón de Sevilla.

Comentarios ¡Únete a la conversación!