Este post es un howto o codekata para consultar sistemas remotos en PHP mediante CURL. Podemos encontrar que con muy pocas líneas consultamos una página web, igualmente a como hacen nuestros navegadores, aunque sin visualizarlas.
De esta forma, podemos conectar con sistemas remotos como APIs RESTful, no sólo websites, y lo que es cada vez más habitual, para interactuar o construir aplicaciones distribuidas entra varios servidores conectados a una red. También se puede usar para crawlear websites así como hacen los principales buscadores o herramientas SEO..
Este post se puede complementar con este otro post sobre APIs RESTful.
También con este otro con algo sobre refactorización.
Empezando con una consulta básica GET
Por ejemplo una consulta GET sencilla a https://jnjsite.com/ podría ser:
<?php
$handler = curl_init('https://jnjsite.com/');
curl_setopt($handler, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($handler, CURLOPT_RETURNTRANSFER, true);
var_dump(curl_exec($handler));
Aquí ya simplemente obtenemos la home de este sitio mediante una petición GET sencilla.
Quitando la cabecera de la respuesta del servidor
Si queremos quitar la cabecera de respuesta de la salida del curl_exec(), podríamos añadir una configuración:
<?php
$handler = curl_init('https://jnjsite.com/');
curl_setopt($handler, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($handler, CURLOPT_RETURNTRANSFER, true);
curl_setopt($handler, CURLOPT_HEADER, false);
var_dump(curl_exec($handler));
Añadiendo la autenticación básica al GET
Otra cosa muy útil es añadir la autenticación básica usando las cabeceras de la petición. Esto se suele hacer usando valores en la forma usuario/clave, o también a veces se pueden llamar clave/secreto en el caso de APIs. Una consulta de esta forma podría quedar así:
<?php
$handler = curl_init('https://jnjsite.com/');
$headers = [
'Content-Type: application/ld+json',
'Authorization: Basic '.base64_encode($this->api_key.':'.$this->api_secret),
];
curl_setopt($handler, CURLOPT_HTTPHEADER, $headers);
curl_setopt($handler, CURLOPT_RETURNTRANSFER, true);
curl_setopt($handler, CURLOPT_HEADER, false);
var_dump(curl_exec($handler));
Una consulta POST
Los siguiente podrían ser preparar consultas POST como las que envían los formularios, o para crear elementos en APIs RESTful:
$handler = curl_init('https://jnjsite.com/endpoint/');
curl_setopt($handler, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($handler, CURLOPT_POSTFIELDS, json_encode($data));
curl_setopt($handler, CURLOPT_RETURNTRANSFER, true);
curl_setopt($handler, CURLOPT_HEADER, false);
var_dump(curl_exec($handler));
Si quisiéramos enviar los datos en ver de como un formulario como datos JSON tal y como se indica en la codificación de los datos, faltaría enviar en la cabecera la indicación siguiente:
curl_setopt($handler, CURLOPT_HTTPHEADER, ['Content-Type:application/json']);
Por defecto CURL envia un Content-Type igual a application/x-www-form-urlencoded si no se especifica lo contrario.
Otra consulta PUT
Las consultas PUT son muy parecidas a las POST, sólo que lo que hacen es reemplazar completamente el objeto que estamos enviando en el caso de APIs RESTful:
$handler = curl_init('https://jnjsite.com/endpoint/123');
curl_setopt($handler, CURLOPT_CUSTOMREQUEST, 'PUT');
curl_setopt($handler, CURLOPT_HTTPHEADER, ['Content-Type:application/json']);
curl_setopt($handler, CURLOPT_POSTFIELDS, json_encode($data));
curl_setopt($handler, CURLOPT_RETURNTRANSFER, true);
curl_setopt($handler, CURLOPT_HEADER, false);
var_dump(curl_exec($handler));
Más abajo en el post lo probamos con una API de pruebas.
Ahora una consulta PATCH
Estas consultas PATCH también son parecidas a las anteriores PUT o POST, pero añadiendo un detalle. Estas consultas PATCH exactamente por definición en APIs RESTful lo que hacen es modificar sólo los valores enviados en la petición para el endpoint enviado, dejando intactos el resto de campos:
$handler = curl_init('https://jnjsite.com/endpoint/123');
curl_setopt($handler, CURLOPT_CUSTOMREQUEST, 'PATCH');
curl_setopt($handler, CURLOPT_HTTPHEADER, ['Content-Type:application/json']);
curl_setopt($handler, CURLOPT_POSTFIELDS, json_encode($data));
curl_setopt($handler, CURLOPT_RETURNTRANSFER, true);
curl_setopt($handler, CURLOPT_HEADER, false);
var_dump(curl_exec($handler));
Más abajo en el post lo probamos con una API de pruebas.
Otra consulta ahora DELETE
Finalmente este tipo de consultas están destinadas en las APIs a borrar items concretos. Si por ejemplo enviáramos un DELETE a un endpoint de la forma /cosa/123 lo que se haría en el destino sería borra la cosa identificada con el ID 123 en la base de datos:
$handler = curl_init('https://jnjsite.com/endpoint/123');
curl_setopt($handler, CURLOPT_CUSTOMREQUEST, 'DELETE');
curl_setopt($handler, CURLOPT_RETURNTRANSFER, true);
curl_setopt($handler, CURLOPT_HEADER, false);
var_dump(curl_exec($handler));
Luego trato de explicar con un ejemplo más abajo todo esto.
Encapsulando todo usando POO
Lo siguiente es seguir las buenas prácticas de programación es usando código limpio y reutilizable. Así que lo más básico sería encapsularlo en un objeto, por ejemplo:
<?php
class TestingUtils
{
private $base_url;
public function __construct($base_url)
{
$this->base_url = $base_url;
}
public function do_get($path_call)
{
// Aquí el código del GET..
}
public function do_post($path_call, $data)
{
// Aquí el código del POST..
}
public function do_put($path_call, $data)
{
// Aquí el código del PUT..
}
public function do_patch($path_call, $data)
{
// Aquí el código del PATCH..
}
public function do_delete($path_call)
{
// Aquí el código del DELETE..
}
}
A partir de aquí ya podríamos usar en el código fuente algo como lo siguiente:
<?php
include 'TestingUtils.php';
$tools = new TestingUtils('https://host-de-pruebas.com');
var_dump($tools->do_get('/'));
var_dump($tools->do_post('/', []));
var_dump($tools->do_put('/endpoint/123', []));
var_dump($tools->do_patch('/endpoint/123', []));
var_dump($tools->do_delete('/endpoint/123'));
Añadiendo un patrón de diseño
Siguiendo con el codekata, podríamos añadir el patrón de diseño Singleton para evitar usar el constructor en repetidas partes del programa, o para tener que declarar un objeto global y compartirlo. Así no tendremos nada más que un objeto. No sé si quizá esto no nos conviene para lo que estemos preparando. Pero en el caso de que sí que queramos sólo un objeto declarado en toda la aplicación, podríamos hacer así:
<?php
class TestingUtils
{
private static $instance;
private $base_url;
private function __construct($base_url)
{
$this->base_url = $base_url;
}
public static function get_instance($base_url = 'https://localhost/')
{
if (!isset(self::$instance)) {
self::$instance = new self($base_url);
}
return self::$instance;
}
public function change_base_url($base_url)
{
$this->base_url = $base_url;
}
// Resto del objeto..
Ahora se podría usar lo anterior de forma muy parecida:
<?php
include 'TestingUtils.php';
$tools = TestingUtils::get_instance('https://host-de-pruebas.com');
var_dump($tools->do_get('/'));
var_dump($tools->do_post('/', []));
$tools->change_base_url('https://host-de-pruebas2.com');
var_dump($tools->do_put('/endpoint/123', []));
var_dump($tools->do_patch('/endpoint/123', []));
var_dump($tools->do_delete('/endpoint/123'));
La clase completa encapsulada
<?php
class TestingUtils
{
private static $instance;
private $base_url;
private function __construct($base_url)
{
$this->base_url = $base_url;
}
public static function get_instance($base_url)
{
if (!isset(self::$instance)) {
self::$instance = new self($base_url);
}
return self::$instance;
}
public function change_base_url($base_url)
{
$this->base_url = $base_url;
}
public function do_get($path_call)
{
$handler = curl_init($this->base_url.$path_call);
curl_setopt($handler, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($handler, CURLOPT_RETURNTRANSFER, true);
curl_setopt($handler, CURLOPT_HEADER, false);
return curl_exec($handler);
}
public function do_post($path_call, $data)
{
$handler = curl_init($this->base_url.$path_call);
curl_setopt($handler, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($handler, CURLOPT_HTTPHEADER, ['Content-Type:application/json']);
curl_setopt($handler, CURLOPT_POSTFIELDS, json_encode($data));
curl_setopt($handler, CURLOPT_RETURNTRANSFER, true);
curl_setopt($handler, CURLOPT_HEADER, false);
return curl_exec($handler);
}
public function do_put($path_call, $data)
{
$handler = curl_init($this->base_url.$path_call);
curl_setopt($handler, CURLOPT_CUSTOMREQUEST, 'PUT');
curl_setopt($handler, CURLOPT_HTTPHEADER, ['Content-Type:application/json']);
curl_setopt($handler, CURLOPT_POSTFIELDS, json_encode($data));
curl_setopt($handler, CURLOPT_RETURNTRANSFER, true);
curl_setopt($handler, CURLOPT_HEADER, false);
return json_decode(curl_exec($handler), true);
}
public function do_patch($path_call, $data)
{
$handler = curl_init($this->base_url.$path_call);
curl_setopt($handler, CURLOPT_CUSTOMREQUEST, 'PATCH');
curl_setopt($handler, CURLOPT_HTTPHEADER, ['Content-Type:application/json']);
curl_setopt($handler, CURLOPT_POSTFIELDS, json_encode($data));
curl_setopt($handler, CURLOPT_RETURNTRANSFER, true);
curl_setopt($handler, CURLOPT_HEADER, false);
return json_decode(curl_exec($handler), true);
}
public function do_delete($path_call)
{
$handler = curl_init($this->base_url.$path_call);
curl_setopt($handler, CURLOPT_CUSTOMREQUEST, 'DELETE');
curl_setopt($handler, CURLOPT_RETURNTRANSFER, true);
curl_setopt($handler, CURLOPT_HEADER, false);
return json_decode(curl_exec($handler), true);
}
}
Probando todo con una API RESTful de pruebas
Siguiendo un codekata anterior, el de este post sobre cómo crear APIs RESTful con Symfony podemos probar a ver si funciona todo. Podríamos hacer algo como lo siguiente para añadir localizaciones dado el caso de usar este tipo de elementos, por ejemplo:
Por ejemplo un GET sobre las localizaciones:
<?php
include 'TestingUtils.php';
$tools = TestingUtils::get_instance('http://localhost:8000/api');
var_dump($tools->do_get('/locations'));
Con respuesta:
Si ahora quisiéramos enviar por ejemplo 100 localizaciones nuevas podríamos hacer unos POSTs:
<?php
include 'TestingUtils.php';
$tools = TestingUtils::get_instance('http://localhost:8000/api');
for($i = 0; $i < 100; $i++){
var_dump($tools->do_post('/locations', [
'latitude' => rand(0,100) / 100,
'longitude' => rand(0,100) / 100,
'comment' => 'Localización para GIS de pruebas',
]));
}
var_dump($tools->do_get('/locations'));
Con respuestas podríamos tener ahora:
Para terminar podemos probar también el DELETE así:
<?php
include 'TestingUtils.php';
$tools = TestingUtils::get_instance('http://localhost:8000/api');
for ($i = 1; $i <= 100; ++$i) {
var_dump($tools->do_delete('/locations/'.$i));
}
var_dump($tools->do_get('/locations'));
Y dejaríamos vacío el GIS de pruebas.