PHP: clase estándar para crawlear webs o consultar APIs RESTful

2021-08-09 - Categorías: PHP
Networking Earth

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.

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

 

© 2021 JnjSite.com - MIT license

Sitio hecho con WordPress, diseño y programación del tema por Jnj.