APP PHPRunner server RESTfull API

En este ejemplo voy a explicar cómo, cualquier aplicación realizada en PHPRunner puede disponer de este interfaz para que otras aplicaciones puedan consumir datos y objetos de negocio.

El ejemplo está hecho en PHPRunnner 10.2, pero podría estar hecho en versión 9.8 u otra, ya que el API es PHP y no es generado por PHPRunner, aunque sí se utilizan recursos de PHPRunner, como es la conexión a la base de datos.

Requisitos funcionales del aplicativo

  1. El sistema debe facilitar la lista de vehículos de segunda mano, que están disponibles para la venta.
  2. Los vehículos tendrán una codificación por Marca y por Modelo.
  3. La aplicación web tendrá un interface para consultar y mantener todos estos datos.
  4. El sistema dispondrá de una Api Restfull, con los siguientes métodos:
    • Lista de todos los vehículos disponibles.
    • Lista de las Marcas y Modelos del catálogo.
    • Alta de un nuevo vehículo y si la Marca y/o Modelo son nuevos, lata de los mismos en los catálogos.

Modelo de Datos

Es un modelo muy simple porque el objetivo del ejemplo es facilitar información del server de Restfull API.

Se ha normalizado para que se vea que se puede tener, sin complejidad, el modelo normalizado y disponer de estas API’s.

 

DEMO: https://fhumanes.com/car

¡¡¡¡ Al migrar esta aplicación a PHP 8.1 he actualizado el framework Slim a versión 4. La anterior era Slim 2.6 !!!!

Descripción del server Restfull API

Para hacer este ejercicio me he apoyado en la información de Federico Guzmán, en su artículo de RESTful API: ¿Cómo hacer un API con PHP y Mysql? y la de Ijeoma Nelson, de su artículo How to Create a RESTful API in PHP With SlimPHP 4 and MySQL . La segunda referencia me ha sido muy útil para utilizar Slim 4, porque es muy diferente ala versión de Slim 2.

Slim 4, requiere PHP 7.4 o superior.

Ambos artículos son muy didácticos y está muy bien explicados, por lo que os pido que lo leáis con detenimiento para conocer el  detalle del ejercicio.

Uno de los aspectos más relevantes son las herramientas de testeo del desarrollo, es decir, clientes de Restfull API que  vamos a utilizar para comprobar el funcionamiento.

  • RESTClient, es un Add-on de Firefox que funciona muy bien
  • Postman, aplicación de escritorio que está en todos los sistemas operativos y que soporta tanto Restfull API como los webservices y otros protocolos de intercambio de datos.

A nivel de ubicación del código del servidor, voy a utilizar el directorio “restapi”, inmediatamente debajo del directorio de la aplicación.

Con lo que el directorio raíz para los métodos de acceso se queda en:  http://localhost/car/restapi/v1/ .

Los métodos de acceso quedan:

  1. Consulta de todos los vehículos de la base de datos .
    GET  http://localhost/car/restapi/v1/auto
  2. Consulta de los catálogos de las Marcas y Modelos
    GET  http://localhost/car/restapi/v1/modelo
  3. Crear un nuevo vehículo. Esta opción requiere autenticación
    POST http://localhost/car/restapi/v1/auto
    En cabecera del mensaje requiere:
    Authorization:  3d524a53c110e4c22463b10ed32cef9d
    Content-Type: application/x-www-form-urlencoded

En Body (cuerpo del mensaje) requiere pasar los parámetros:

make=Citroen&model=Sara&year=2010&msrp=2563.15

Partiendo del ejemplo de Federico Guzmán he modificado su código para disponer de la persistencia de los datos en una base de datos de MySQL utilizando la misma  conexión que tenemos en la aplicación de PHPRunner.

Así pues los códigos han quedado:

index.php . El programa principal de la gestión de las peticiones.

<?php

/**
 *
 * @About:      API Interface
 * @File:       index.php
 * @Date:       $Date:$ Agosot0 -2022
 * @Version:    $Rev:$ 1.0
 * @Developer:  Federico Guzman || Modificado por Fernando Humanes para PHP 8.1
 **/

/* Los headers permiten acceso desde otro dominio (CORS) a nuestro REST API o desde un cliente remoto via HTTP
 * Removiendo las lineas header() limitamos el acceso a nuestro RESTfull API a el mismo dominio
 * Nótese los métodos permitidos en Access-Control-Allow-Methods. Esto nos permite limitar los métodos de consulta a nuestro RESTfull API
 * Mas información: https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS
 **/
header("Access-Control-Allow-Origin: *");
header('Access-Control-Allow-Credentials: true');
header('Access-Control-Allow-Methods: PUT, GET, POST, DELETE, OPTIONS');
header("Access-Control-Allow-Headers: X-Requested-With");
header('Content-Type: text/html; charset=utf-8');
header('P3P: CP="IDC DSP COR CURa ADMa OUR IND PHY ONL COM STA"');

include_once '../include/Config.php';

require_once("../../include/dbcommon.php"); // DataBase PHPRunner

// use App\Models\Db;  // Utilizamos la conexión de PHPRunner
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Selective\BasePath\BasePathMiddleware;
use Slim\Factory\AppFactory;

require_once __DIR__ . '/../libs/autoload.php';



$app = AppFactory::create();


$app->addRoutingMiddleware();
// $app->add(new BasePathMiddleware($app)); // No usar si se ejecuta en subdirectorio
$app->addErrorMiddleware(true, true, true);
$app->addBodyParsingMiddleware();

$app->setBasePath(SCRIPTS_DIR);

// --------------------------------------------------------------------------------------

/* Usando GET para consultar los autos */

$app->get('/auto', function(Request $request, Response $response) {
    
    $responseBody = array();
    $autos = array();

    $sql="SELECT brand.`Brand`,model.`Model`,car.`Year`,car.`Price` FROM car
    join brand on (car.`brand_idbrand` = brand.`idbrand`)
    join model on (car.`model_idmodel` = model.`idmodel`)
    order by 1,2,3,4";
    try {
    global $conn;

    $rsSql=$conn->query($sql);
    while ($data = $rsSql->fetch_array(MYSQLI_ASSOC)){
      $importe = number_format($data['Price'], 2, '.', ',');
      $autos[] = array('make'=>$data['Brand'], 'model'=>$data['Model'], 'year'=>$data['Year'], 'MSRP'=>$importe);
    }
    $responseBody["error"] = false;
    $responseBody["message"] = "Autos cargados: " . count($autos); //podemos usar count() para conocer el total de valores de un array
    $responseBody["autos"] = $autos;

    $response->getBody()->write(json_encode($responseBody));
    return $response
        ->withHeader('content-type', 'application/json')
        ->withStatus(200);

    } catch (mysqli_sql_exception $e) {
        $error = array(
            "message" => $e->getMessage()
        );
        $response->getBody()->write(json_encode($error));
        return $response
            ->withHeader('content-type', 'application/json')
            ->withStatus(500);
    }
});

// --------------------------------------------------------------------------------------

/* Usando POST para crear un auto */

$app->post('/auto', function(Request $request, Response $response) {
    // Verify Token Security
    $verify = authenticate($request, $response);
    if (is_array($verify)) { // Si es una array, es que hay error
        $response->getBody()->write(json_encode($verify));
        return $response
            ->withHeader('content-type', 'application/json')
            ->withStatus(400);
    }
    // check for required params
    $verify = verifyRequiredParams(array('make', 'model', 'year','msrp'), $request, $response);
    if (is_array($verify)) { // Si es una array, es que hay error
        $response->getBody()->write(json_encode($verify));
        return $response
            ->withHeader('content-type', 'application/json')
            ->withStatus(400);
    }
    $data = $request->getParsedBody();

    $responseBody = array();
    $param = array();
    //capturamos los parametros recibidos y los almacxenamos como un nuevo array
    $param['make']  = $data['make'];
    $param['model'] = $data['model'];
    $param['year']  = $data['year'];
    $param['msrp']  = $data['msrp'];
    
    /* Podemos inicializar la conexion a la base de datos si queremos hacer uso de esta para procesar los parametros con DB */
    require_once '../include/DbAuto.php';
    $db = new DbAuto();

    /* Podemos crear un metodo que almacene el nuevo auto, por ejemplo: */
    $auto = $db->createAuto($param);

    if ( is_array($auto) ) {
        $responseBody["error"] = false;
        $responseBody["message"] = "Auto creado satisfactoriamente!";
        $responseBody["auto"] = $param;
        $response->getBody()->write(json_encode($responseBody));
        return $response
            ->withHeader('content-type', 'application/json')
            ->withStatus(200);
    } else {
        $responseBody["error"] = true;
        $responseBody["message"] = "Error al crear auto. Por favor intenta nuevamente.";
        return $response
        ->withHeader('content-type', 'application/json')
        ->withStatus(400);
    }
});

// --------------------------------------------------------------------------------------

/* Usando GET para consultar los modelos */

$app->get('/modelo', function(Request $request, Response $response) {
    
    $responseBody = array();
    $modelos = array();

    global $conn;

    $sql="SELECT brand.`Brand`, model.`Model` 
          FROM brand 
          join model on (model.`brand_idbrand` = brand.`idbrand`) 
          order by 1,2";
    $rsSql= DB::Query($sql);

    while ($data = $rsSql->fetchAssoc()){
      $modelos[] = array('make'=>$data['Brand'], 'model'=>$data['Model']);
    }   
    $responseBody["error"] = false;
    $responseBody["message"] = "Modelos cargados: " . count($modelos); //podemos usar count() para conocer el total de valores de un array
    $responseBody["modelos"] = $modelos;

    $response->getBody()->write(json_encode($responseBody));
    return $response
        ->withHeader('content-type', 'application/json')
        ->withStatus(200);
});

// --------------------------------------------------------------------------------------


/* corremos la aplicación */
$app->run();

/*********************** USEFULL FUNCTIONS **************************************/

/**
 * Verificando los parametros requeridos en el metodo
 */
function verifyRequiredParams($required_fields, Request  $request, Response $response)
{
    $error = false;
    $error_fields = "";

    $request_params = $request->getParsedBody();

    foreach ($required_fields as $field) {
        if (!isset($request_params[$field]) || strlen(trim($request_params[$field])) <= 0) {
            $error = true;
            $error_fields .= $field . ', ';
        }
    }

    if ($error) {
        // Required field(s) are missing or empty
        // echo error json and stop the app
        $responseBody = array();
        $responseBody["error"] = true;
        $responseBody["message"] = 'Required field(s) ' . substr($error_fields, 0, -2) . ' is missing or empty';

        return $responseBody;
    }
    return true;
}

/**
 * Validando parametro email si necesario; un Extra ;)
 */
function validateEmailRest($email, Request $request, Response $response)
{
    $responseBody = array();
    if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
        $responseBody["error"] = true;
        $responseBody["message"] = 'Email address is not valid';
        return $responseBody;
    }
    return true;
}



/**
 * Revisa si la consulta contiene un Header "Authorization" para validar
 */
function authenticate(Request $request, Response $response)
{
    // Getting request headers
    $headers = $request->getHeaders();
    // Verifying Authorization Header
    if (isset($headers['Authorization'])) {
        // get the api key
        $token = $headers['Authorization'];

        // validating api key
        if (!($token[0] == API_KEY)) { //API_KEY declarada en Config.php

            // api key is not present in users table
            $responseBody["error"] = true;
            $responseBody["message"] = "Acceso denegado. Token inválido";
            // Error 401
            return $responseBody;
        } else {
            //procede utilizar el recurso o metodo del llamado
            return true;
        }
    } else {
        // api key is missing in header
        $responseBody["error"] = true;
        $responseBody["message"] = "Falta token de autorización";
        // error 400
        return $responseBody;
    }
}

config.php. La clave API de control de la aplicación.

<?php

//
//  Database configuration
//

// En este caso, estas definiciones no se están usando
// define('DB_USERNAME', 'root');
// define('DB_PASSWORD', 'humanes');
// define('DB_HOST', 'localhost');
// define('DB_NAME', 'car');

//
//  URI raíz de la aplicación
//

define('SCRIPTS_DIR', '/car/restapi/v1');  

 

//referencia generado con MD5(uniqueid(<some_string>, true))
define('API_KEY','3d524a53c110e4c22463b10ed32cef9d');

/**
 * API Response HTTP CODE
 * Used as reference for API REST Response Header
 *
 */
/*
200 	OK
201 	Created
304 	Not Modified
400 	Bad Request
401 	Unauthorized
403 	Forbidden
404 	Not Found
422 	Unprocessable Entity
500 	Internal Server Error
*/

?>

DbAuto.php . La clase que nos va a facilitar el alta de los nuevos vehículos.

<?php

/**
 *
 * @About:      ADD of a new car
 * @File:       DbAuto.php
 * @Date:       $Date:$ April 2020
 * @Version:    $Rev:$ 1.0
 * @Developer:  fernando humanes
 **/
class DbAuto
{

    private $connDB;
    // Field 'make', 'model', 'year', 'msrp'

    function __construct()
    {
        global $conn; // Connection data base of PHPRunner
        $this->connDB = $conn;
    }

    public function createAuto($param)
    {   
        global $conn; // Connection data base of PHPRunner
        $id_make = $this->buscaMake($param['make']);
        $id_model = $this->buscaModel($id_make, $param['model']);
        $year = $param['year'];
        $msrp = $param['msrp'];
        $sql = "INSERT INTO car (brand_idbrand, model_idmodel, `Year`, Price) VALUES ($id_make,$id_model,$year,$msrp);";
        $rsSql = DB::Query($sql);
        return $param;
    }

    private function buscaMake($make)
    {
        global $conn; // Connection data base of PHPRunner
        $sql = "SELECT idbrand, Brand FROM brand where upper(Brand) = upper('$make')";
        $rsSql = DB::Query($sql);
        $row_cnt = $rsSql->value("idbrand");
        $row = $rsSql->fetchAssoc();

        if ($row_cnt == null ) {  //  If it does not exist, a new catalog is registered
            $sql = "INSERT INTO brand (Brand) VALUES ('$make')";
            $rsSql = DB::Query($sql);
            $id = DB::LastId();
        } else {
            $id = $row['idbrand'];
        }
        return $id;
    }

    private function buscaModel($id_make, $model)
    {
        global $conn; // Connection data base of PHPRunner
        $sql = "SELECT idmodel, brand_idbrand, Model FROM model where brand_idbrand = $id_make and upper(Model) = upper('$model')";
        $rsSql = DB::Query($sql);
        $row_cnt = $rsSql->value("idmodel");
        $row = $rsSql->fetchAssoc();

        if ($row_cnt == null ) { // If it does not exist, a new catalog is registered
            $sql = "INSERT INTO model (brand_idbrand, Model) VALUES ($id_make,'$model')";
            $rsSql = DB::Query($sql);
            $id = DB::LastId();
        } else {
            $id = $row['idmodel'];
        }
        return $id;
    }
}

Os adjunto todos los fuentes del ejemplo y como siempre, para cualquier duda o necesidad que os surja de este ejemplo, contactar conmigo en [email protected]

 

 

Blog personal para facilitar soporte gratuito a usuarios de PHPRunner