Creación de API REST para proyectos PHP heredados

Publicado: 2022-03-11

Crear o diseñar una API REST no es una tarea fácil, especialmente cuando tiene que hacerlo para proyectos PHP heredados. Hoy en día, hay muchas bibliotecas de terceros que facilitan la implementación de una API REST, pero integrarlas en las bases de código heredadas existentes puede ser bastante desalentador. Y no siempre tienes el lujo de trabajar con marcos modernos, como Laravel y Symfony. Con proyectos PHP heredados, a menudo puede encontrarse en algún lugar en medio de marcos internos obsoletos, ejecutándose sobre versiones antiguas de PHP.

Creación de API REST para proyectos PHP heredados

Creación de API REST para proyectos PHP heredados
Pío

En este artículo, veremos algunos desafíos comunes al intentar implementar API REST desde cero, algunas formas de solucionar esos problemas y una estrategia general para crear servidores de API personalizados basados ​​en PHP para proyectos heredados.

Aunque el artículo se basa en PHP 5.3 y superior, los conceptos básicos son válidos para todas las versiones de PHP posteriores a la versión 5.0 e incluso se pueden aplicar a proyectos que no sean de PHP. Aquí, no cubriremos qué es una API REST en general, por lo que si no está familiarizado con ella, asegúrese de leer sobre ella primero.

Para facilitarle el seguimiento, aquí hay una lista de algunos términos utilizados a lo largo de este artículo y sus significados:

  • Servidor API: principal aplicación REST que da servicio a la API, en este caso, escrita en PHP.
  • Punto final de API: un "método" de back-end con el que el cliente se comunica para realizar una acción y producir resultados.
  • URL de punto final de API: URL a través de la cual el sistema de backend es accesible para el mundo.
  • Token API: un identificador único que se pasa a través de encabezados HTTP o cookies a partir del cual se puede identificar al usuario.
  • Aplicación: aplicación cliente que se comunicará con la aplicación REST a través de los puntos finales de la API. En este artículo supondremos que está basado en la web (ya sea de escritorio o móvil), por lo que está escrito en JavaScript.

Pasos iniciales

Patrones de ruta

Una de las primeras cosas que debemos decidir es en qué ruta de URL estarán disponibles los puntos finales de la API. Hay 2 formas populares:

  • Cree un nuevo subdominio, como api.example.com.
  • Cree una ruta, como ejemplo.com/api.

De un vistazo, puede parecer que la primera variante es más popular y atractiva. Sin embargo, en realidad, si está creando una API específica para un proyecto, podría ser más apropiado elegir la segunda variante.

Una de las razones más importantes detrás de adoptar el segundo enfoque es que esto permite que las cookies se utilicen como un medio para transferir credenciales. Los clientes basados ​​en navegador enviarán automáticamente las cookies apropiadas dentro de las solicitudes XHR, eliminando la necesidad de un encabezado de autorización adicional.

Otra razón importante es que no necesita hacer nada con respecto a la configuración del subdominio o los problemas de administración donde algunos servidores proxy pueden eliminar los encabezados personalizados. Esto puede ser una prueba tediosa en proyectos heredados.

El uso de cookies puede considerarse una práctica "no REST", ya que las solicitudes REST no deben tener estado. En este caso, podemos comprometernos y pasar el valor del token en una cookie en lugar de pasarlo a través de un encabezado personalizado. Efectivamente, estamos usando cookies solo como una forma de pasar el valor del token en lugar del session_id directamente. Este enfoque podría considerarse sin estado, pero podemos dejarlo a sus preferencias.

También se pueden versionar las URL de punto final de la API. Además, pueden incluir el formato de respuesta esperado como una extensión en el nombre de la ruta. Aunque estos no son críticos, especialmente durante las primeras etapas del desarrollo de la API, a largo plazo estos detalles sin duda pueden dar sus frutos. Especialmente cuando necesitas implementar nuevas características. Verificar qué versión espera el cliente y proporcionar el formato necesario para la compatibilidad con versiones anteriores puede ser la mejor solución.

La estructura de la URL del punto final de la API podría tener el siguiente aspecto:

 example.com/api/${version_code}/${actual_request_path}.${format}

Y, un ejemplo real:

 example.com/api/v1.0/records.json

Enrutamiento

Después de elegir una URL base para los extremos de la API, lo siguiente que debemos hacer es pensar en nuestro sistema de enrutamiento. Podría integrarse en un marco existente, pero si eso es demasiado engorroso, una posible solución es crear una carpeta llamada "api" en la raíz del documento. De esa manera, la API puede tener una lógica completamente separada. Puede ampliar este enfoque colocando la lógica de la API en sus propios archivos, como este:

Puede pensar en "www/api/Apis/Users.php" como un "controlador" separado para un punto final de API en particular. Sería genial reutilizar implementaciones del código base existente, por ejemplo, reutilizar modelos que ya están implementados en el proyecto para comunicarse con la base de datos.

Finalmente, asegúrese de apuntar todas las solicitudes entrantes desde “/api/*” a “/api/index.php”. Esto se puede hacer cambiando la configuración de su servidor web.

Clase de API

Versión y formato

Siempre debe definir claramente qué versiones y formatos aceptan sus puntos finales de API y cuáles son los predeterminados. Esto le permitirá crear nuevas funciones en el futuro mientras mantiene las funcionalidades antiguas. La versión de la API puede ser básicamente una cadena, pero puede usar valores numéricos para una mejor comprensión y comparabilidad. Es bueno tener dígitos de repuesto para versiones menores porque indicaría claramente que solo algunas cosas son diferentes:

  • v1.0 significaría la primera versión.
  • Primera versión v1.1 con algunos cambios menores.
  • v2.0 sería una versión completamente nueva.

El formato puede ser cualquier cosa que su cliente necesite, incluidos, entre otros, JSON, XML e incluso CSV. Al proporcionarlo a través de la URL como una extensión de archivo, la URL del punto final de la API garantiza la legibilidad y se convierte en una obviedad para el consumidor de la API para saber qué formato puede esperar:

  • "/api/v1.0/records.json" devolvería una matriz de registros JSON
  • “/api/v1.0/records.xml” devolvería un archivo XML de registros

Vale la pena señalar que también deberá enviar un encabezado de tipo de contenido adecuado en la respuesta para cada uno de estos formatos.

Al recibir una solicitud entrante, una de las primeras cosas que debe hacer es verificar si el servidor API admite la versión y el formato solicitados. En su método principal, que maneja la solicitud entrante, analice $_SERVER['PATH_INFO'] o $_SERVER['REQUEST_URI'] para determinar si el formato y la versión solicitados son compatibles. Luego, continúe o devuelva una respuesta 4xx (p. ej., 406 "No aceptable"). La parte más crítica aquí es devolver siempre algo que el cliente espera. Una alternativa a esto sería marcar el encabezado de la solicitud "Aceptar" en lugar de la extensión de la ruta de la URL.

Rutas Permitidas

Podría reenviar todo de forma transparente a sus controladores API, pero podría ser mejor usar un conjunto de rutas permitidas incluidas en la lista blanca. Esto reduciría un poco la flexibilidad, pero proporcionará una visión muy clara de cómo se ven las URL de punto final de la API la próxima vez que regrese al código.

 private $public_routes = array( 'system' => array( 'regex' => 'system', ), 'records' => array( 'regex' => 'records(?:/?([0-9]+)?)', ), );

También puede moverlos a archivos separados para hacer las cosas más limpias. La configuración anterior se utilizará para habilitar las solicitudes a estas URL:

 /api/v1.0/system.json /api/v1.0/records.json /api/v1.0/records/7.json

Manejo de datos PUT

PHP maneja automáticamente los datos POST entrantes y los coloca en $_POST superglobal. Sin embargo, ese no es el caso con las solicitudes PUT. Todos los datos están "enterrados" en php://input . No olvide analizarlo en una estructura o matriz separada antes de invocar el método API real. Un simple parse_str podría ser suficiente, pero si el cliente está enviando una solicitud de varias partes, es posible que se necesite un análisis adicional para manejar los límites del formulario. El caso de uso típico de las solicitudes de varias partes incluye la carga de archivos. La detección y el manejo de solicitudes de varias partes se pueden realizar de la siguiente manera:

 self::$input = file_get_contents('php://input'); // For PUT/DELETE there is input data instead of request variables if (!empty(self::$input)) { preg_match('/boundary=(.*)$/', $_SERVER['CONTENT_TYPE'], $matches); if (isset($matches[1]) && strpos(self::$input, $matches[1]) !== false) { $this->parse_raw_request(self::$input, self::$input_data); } else { parse_str(self::$input, self::$input_data); } }

Aquí, parse_raw_request podría implementarse como:

 /** * Helper method to parse raw requests */ private function parse_raw_request($input, &$a_data) { // grab multipart boundary from content type header preg_match('/boundary=(.*)$/', $_SERVER['CONTENT_TYPE'], $matches); $boundary = $matches[1]; // split content by boundary and get rid of last -- element $a_blocks = preg_split("/-+$boundary/", $input); array_pop($a_blocks); // loop data blocks foreach ($a_blocks as $id => $block) { if (empty($block)) { continue; } // parse uploaded files if (strpos($block, 'application/octet-stream') !== false) { // match "name", then everything after "stream" (optional) except for prepending newlines preg_match("/name=\"([^\"]*)\".*stream[\n|\r]+([^\n\r].*)?$/s", $block, $matches); // parse all other fields } else { // match "name" and optional value in between newline sequences preg_match('/name=\"([^\"]*)\"[\n|\r]+([^\n\r].*)?\r$/s', $block, $matches); } $a_data[$matches[1]] = $matches[2]; } }

Con esto, podemos tener la carga útil de solicitud necesaria en Api::$input como entrada sin formato y Api::$input_data como matriz asociativa.

Fingiendo PONER/ELIMINAR

A veces puede verse en una situación en la que el servidor no admite nada más que los métodos estándar GET/POST HTTP. Una solución común a este problema es "falsificar" PUT/DELETE o cualquier otro método de solicitud personalizado. Para eso puedes usar un parámetro “mágico”, como “_method”. Si lo ve en su matriz $_REQUEST , simplemente suponga que la solicitud es del tipo especificado. Los marcos modernos como Laravel tienen esa funcionalidad incorporada. Proporciona una gran compatibilidad en caso de que su servidor o cliente tenga limitaciones (por ejemplo, una persona está usando la red Wi-Fi de su trabajo detrás de un proxy corporativo que no permite solicitudes PUT).

Reenvío a API específica

Si no puede darse el lujo de reutilizar los cargadores automáticos de proyectos existentes, puede crear uno propio con la ayuda de la función spl_autoload_register . Defínalo en su página “api/index.php” y llame a su clase API ubicada en “api/Api.php”. La clase API actúa como un middleware y llama al método real. Por ejemplo, una solicitud a "/api/v1.0/records/7.json" debería terminar invocando el método GET "Apis/Records.php" con el parámetro 7. Esto aseguraría la separación de preocupaciones y proporcionaría una manera de mantener el limpiador lógico. Por supuesto, si es posible integrar esto más profundamente en el marco que está utilizando y reutilizar sus controladores o rutas específicas, también debe considerar esa posibilidad.

Ejemplo “api/index.php” con cargador automático primitivo:

 <?php // Let's define very primitive autoloader spl_autoload_register(function($classname){ $classname = str_replace('Api_', 'Apis/', $classname); if (file_exists(__DIR__.'/'.$classname.'.php')) { require __DIR__.'/'.$classname.'.php'; } }); // Our main method to handle request Api::serve();

Esto cargará nuestra clase Api y comenzará a servirla independientemente del proyecto principal.

OPCIONES Solicitudes

Cuando un cliente usa un encabezado personalizado para reenviar su token único, el navegador primero debe verificar si el servidor admite ese encabezado. Ahí es donde entran las solicitudes de OPCIONES. Su propósito es garantizar que todo esté bien y sea seguro tanto para el cliente como para el servidor API. Entonces, la solicitud de OPCIONES podría activarse cada vez que un cliente intenta hacer algo. Sin embargo, cuando un cliente utiliza cookies para las credenciales, evita que el navegador tenga que enviar esta solicitud adicional de OPCIONES.

Si un cliente solicita POST /users/8.json con cookies, su solicitud será bastante estándar:

  • La aplicación realiza una solicitud POST a /users/8.json.
  • El navegador realiza la solicitud y recibe una respuesta.

Pero con autorización personalizada o encabezado de token:

  • La aplicación realiza una solicitud POST a /users/8.json.
  • El navegador deja de procesar la solicitud e inicia una solicitud de OPCIONES en su lugar.
  • La solicitud de OPCIONES se envía a /users/8.json.
  • El navegador recibe una respuesta con una lista de todos los métodos y encabezados disponibles, según lo define la API.
  • El navegador continúa con la solicitud POST original solo si el encabezado personalizado está presente en la lista de encabezados disponibles.

Sin embargo, tenga en cuenta que incluso cuando use cookies, con PUT/DELETE aún podría recibir esa solicitud adicional de OPCIONES. Así que prepárate para responder.

API de registros

Estructura basica

Nuestro ejemplo de API de Registros es bastante sencillo. Contendrá todos los métodos de solicitud y devolverá la salida a la misma clase de API principal. Por ejemplo:

 <?php class Api_Records { public function __construct() { // In here you could initialize some shared logic between this API and rest of the project } /** * Get individual record or records list */ public function get($id = null) { if ($id) { return $this->getRecord(intval($id)); } else { return $this->getRecords(); } } /** * Update record */ public function put($record_id = null) { // In real world there would be call to model with validation and probably token checking // Use Api::$input_data to update return Api::responseOk('OK', array()); } // ...

Por lo tanto, definir cada método HTTP nos permitirá crear API en estilo REST más fácilmente.

Formato de salida

Responder ingenuamente con todo lo recibido de la base de datos al cliente puede tener consecuencias catastróficas. Para evitar cualquier exposición accidental de datos, cree un método de formato específico que devuelva solo las claves incluidas en la lista blanca.

Otro beneficio de las claves incluidas en la lista blanca es que puede escribir documentación basada en ellas y hacer todas las comprobaciones de tipos para garantizar, por ejemplo, que user_id siempre sea un número entero, que el indicador is_banned siempre sea booleano verdadero o falso, y que las fechas y las horas tengan un estándar formato de respuesta

Salida de resultados

Encabezados

Los métodos separados para la salida de encabezados garantizarán que todo lo que se envíe al navegador sea correcto. Este método puede utilizar los beneficios de hacer que la API sea accesible a través del mismo dominio y, al mismo tiempo, mantener la posibilidad de recibir un encabezado de autorización personalizado. La elección entre el mismo dominio o el de un tercero puede ocurrir con la ayuda de los encabezados de servidor HTTP_ORIGIN y HTTP_REFERER. Si la aplicación detecta que el cliente está utilizando la autorización x (o cualquier otro encabezado personalizado), debe permitir el acceso desde todos los orígenes, permitir el encabezado personalizado. Así que podría verse así:

 header('Access-Control-Allow-Origin: *'); header('Access-Control-Expose-Headers: x-authorization'); header('Access-Control-Allow-Headers: origin, content-type, accept, x-authorization'); header('X-Authorization: '.YOUR_TOKEN_HERE);

Sin embargo, si el cliente está utilizando credenciales basadas en cookies, los encabezados podrían ser un poco diferentes, permitiendo solo encabezados relacionados con el host solicitado y las cookies para las credenciales:

 header('Access-Control-Allow-Origin: '.$origin); header('Access-Control-Expose-Headers: set-cookie, cookie'); header('Access-Control-Allow-Headers: origin, content-type, accept, set-cookie, cookie'); // Allow cookie credentials because we're on the same domain header('Access-Control-Allow-Credentials: true'); if (strtolower($_SERVER['REQUEST_METHOD']) != 'options') { setcookie(TOKEN_COOKIE_NAME, YOUR_TOKEN_HERE, time()+86400*30, '/', '.'.$_SERVER['HTTP_HOST']); }

Tenga en cuenta que la solicitud de OPCIONES no admite cookies, por lo que la aplicación no las enviará con ella. Y, finalmente, esto permite que todos nuestros métodos HTTP deseados tengan vencimiento del control de acceso:

 header('Access-Control-Allow-Methods: POST, GET, OPTIONS, PUT, DELETE'); header('Access-Control-Max-Age: 86400');

Cuerpo

El cuerpo en sí debe contener la respuesta en un formato solicitado por su cliente con un estado HTTP 2xx en caso de éxito, estado 4xx en caso de falla debido al cliente y estado 5xx en caso de falla debido al servidor. La estructura de la respuesta puede variar, aunque especificar los campos de "estado" y "respuesta" también podría ser beneficioso. Por ejemplo, si el cliente está tratando de registrar un nuevo usuario y el nombre de usuario ya está en uso, podría enviar una respuesta con el estado HTTP 200 pero un JSON en el cuerpo que se parece a:

 {“status”: “ERROR”, “response”: ”username already taken”}

… en lugar del error HTTP 4xx directamente.

Conclusión

No hay dos proyectos exactamente iguales. La estrategia descrita en este artículo puede o no ser adecuada para su caso, pero los conceptos básicos deben ser similares, no obstante. Vale la pena señalar que no todas las páginas pueden tener las últimas tendencias o un marco actualizado detrás y, a veces, la ira sobre "por qué mi paquete REST Symfony no funciona aquí" puede convertirse en una motivación para construir algo útil. algo que funciona El resultado final puede no ser tan brillante, ya que siempre será una implementación personalizada y específica del proyecto, pero al final del día la solución será algo que realmente funcione; y en un escenario como este, ese debería ser el objetivo de todo desarrollador de API.

Las implementaciones de ejemplo de los conceptos discutidos aquí se han cargado en un repositorio de GitHub para mayor comodidad. Es posible que no desee utilizar estos códigos de muestra directamente en producción tal como están, pero esto podría funcionar fácilmente como punto de partida para su próximo proyecto de integración de API de PHP heredado.

¿Tuvo que implementar un servidor API REST para algún proyecto heredado recientemente? Comparta su experiencia con nosotros en la sección de comentarios a continuación.