Usar las tablas de Azure Storage en PHP

Hoy le toca al turno a las tablas de Azure Storage. Estas son comunmente utilizadas para almacenar registros de logs, por su gran escalabilidad en cuanto a almacenamiento se refiere. Diferentes servicios de Azure las utilizan para este fin.
Al igual que hice en el articulo de cómo trabajar con los blobs en PHP, hoy voy a crear un proyecto de prueba con Symfony para las tablas. Puedes usar este comando para crear uno:

composer create-project symfony/website-skeleton azure-storage-tables-php

Esta vez, vamos a instalar la librería correspondiente a las tablas, del repositorio Microsoft Azure Storage PHP Client Libraries, llamada azure-storage-table.

composer require microsoft/azure-storage-table

También vamos a crear, al igual que en el ejemplo de los blobs, una clase llamada TableService, en src > Service, con el siguiente contenido:

<?php
namespace App\Service;
use MicrosoftAzure\Storage\Common\Exceptions\ServiceException;
use MicrosoftAzure\Storage\Table\Models\EdmType;
use MicrosoftAzure\Storage\Table\Models\Entity;
use MicrosoftAzure\Storage\Table\TableRestProxy;
use Psr\Log\LoggerInterface;
use Ramsey\Uuid\Uuid;
class TableService
{
    private $logger;
    private $tableClient;
    public function __construct(LoggerInterface $logger)
    {
        $this->logger = $logger;
        $this->tableClient = TableRestProxy::createTableService($_SERVER['AZURE_STORAGE_CONNECTION_STRING']);
    }
    private function createTableIfNotExists($tableName)
    {
        try {
            $this->tableClient->getTable($tableName);
        } catch (ServiceException $exception) {
            if ($exception->getCode() == 404) {
                try {
                    $this->tableClient->createTable($tableName);
                } catch (ServiceException $exception) {
                    $this->logger->error('failed to get the entities: ' . $exception->getCode() . ':' . $exception->getMessage());
                    throw $exception;
                }
            }
        }
    }
    public function getEntities($table = 'people')
    {
        $filter = "PartitionKey eq 'BreakingBad'";
        try {
            $this->createTableIfNotExists($table);
            $result = $this->tableClient->queryEntities($table, $filter);
            return $result->getEntities();
        } catch (ServiceException $exception) {
            $this->logger->error('failed to get the entities: ' . $exception->getCode() . ':' . $exception->getMessage());
            throw $exception;
        }
    }
    public function createEntity($firstName, $lastName, $table = 'people')
    {
        try {
            $this->createTableIfNotExists($table);
            $uuid1 = Uuid::uuid1();
            $entity = new Entity();
            $entity->setPartitionKey("BreakingBad");
            $entity->setRowKey($uuid1->toString());
            $entity->addProperty("firstName", EdmType::STRING, $firstName);
            $entity->addProperty("lastName", EdmType::STRING, $lastName);
            $this->tableClient->insertEntity($table, $entity);
        } catch (ServiceException $exception) {
            $this->logger->error('failed to get the entities: ' . $exception->getCode() . ':' . $exception->getMessage());
            throw $exception;
        }
    }
}

En este ejemplo sólo muestro cómo crear la conexión con el cliente para las tablas, a través del constructor, así como recuperar entidades y su creación. En este artículo puedes saber más de las tablas, pero básicamente en ellas puedes almacenar entidades sin un esquema fijo de hasta 254 propiedades o columnas, de las cuales dos deben ser obligatoriamente la Primary Key y Row Key. En la mayoría de los logs que puedes ver de los servicios de Azure, la Primary Key suele ser la fecha del registro y la Row Key puede ser el evento que provocó dicho registro.

Ejemplo de Partition Key y Row Key en una tabla de Azure Storage.PNG
Ejemplo de Partition Key y Row Key en una tabla de Azure Storage

Esto es importante porque es lo que nos va a ayudar después tanto a recuperar como a filtrar entidades cuando hagamos búsquedas sobre la tabla. Al no haber indices, es importante que definas estas columnas bien para que el acceso sea lo más rápido posible. En este caso, al ser un ejemplo, estoy utilizando como Primary Key el nombre de la serie y como Row Key un uuid para el que he utilizado la librería ramsey/uuid.
La cadena de conexión se indicará en el archivo .env y debe tener el siguiente formato:

AZURE_STORAGE_CONNECTION_STRING=DefaultEndpointsProtocol=https;AccountName=YOUR_ACCOUNT_NAME;AccountKey=YOUR_ACCOUNT_KEY;

Ahora crea un controlador llamado TablesController:

php bin/console make:controller TablesController

Reemplaza el contenido con el siguiente código, donde tienes las acciones para recuperar las entidades y también crearlas:

<?php
namespace App\Controller;
use App\Service\TableService;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
class TablesController extends AbstractController
{
    /**
     * @Route("/", name="tables")
     */
    public function index(TableService $table)
    {
        return $this->render('tables/index.html.twig', [
            'controller_name' => 'TablesController',
            'entities' => $table->getEntities(),
        ]);
    }
    /**
     * @Route("/add/entity")
     */
    public function create(Request $request, TableService $table)
    {
        $table->createEntity($request->get('firstName'), $request->get('lastName'));
        return $this->redirectToRoute('tables');
    }
}

Por último, he modificado el archivo templates/base.html.twig para añadir la librería de Bootstrap:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>{% block title %}Welcome!{% endblock %}</title>
        {% block stylesheets %}
        <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.2.1/css/bootstrap.min.css" integrity="sha384-GJzZqFGwb1QTTN6wy59ffF1BuGJpLSa9DkKMp0DgiMDm4iYMj70gZWKYbI706tWS" crossorigin="anonymous">
        {% endblock %}
    </head>
    <body>
    <div class="container">
        {% block body %}{% endblock %}
     </div>
        {% block javascripts %}{% endblock %}
    </body>
</html>

y también el archivo templates/tables/index.html.twig para poder crear y listar las entidades de people.

{% extends 'base.html.twig' %}
{% block title %}Hello TablesController!{% endblock %}
{% block body %}
<div class="row">
    <div class="col-lg-12 jumbotron">
        <h1>Azure Table storage</h1>
        <p class="lead">You can use Table storage to store flexible datasets like user data for web applications, address books, device information, or other types of metadata your service requires. You can store any number of entities in a table, and a storage account may contain any number of tables, up to the capacity limit of the storage account.</p>
    </div>
</div>
<div class="row">
    <div class="col">
        <form action="/add/entity" method="post" class="form-inline">
          <label class="sr-only" for="firstName">First Name</label>
          <input type="text" class="form-control mb-2 mr-sm-2" id="firstName" name="firstName" placeholder="First name">
          <label class="sr-only" for="lastName">Last Name</label>
          <input type="text" class="form-control mb-2 mr-sm-2" id="lastName" name="lastName" placeholder="Last name">
          <button type="submit" class="btn btn-primary mb-2">Submit</button>
        </form>
    </div>
</div>
<div class="row" style="margin-top:10px">
     {% for entity in entities %}
        <div class="col-sm-3" style="margin-bottom:5px">
            <div class="list-group">
                <a hred="#" class="list-group-item list-group-item-action active">{{entity.getPartitionKey()}}</a>
                <a href="#" class="list-group-item list-group-item-action"><strong>First name</strong>: {{entity.getProperty("firstName").getValue()}}</a>
                <a href="#" class="list-group-item list-group-item-action"><strong>Last name</strong>: {{entity.getProperty("lastName").getValue()}}</a>
            </div>          
        </div>
      {% endfor %}
</div>
{% endblock %}

El resultado será algo parecido a lo siguiente:

Trabajar con tablas de Azure Storage en PHP
Trabajar con tablas de Azure Storage en PHP

El ejemplo completo lo tienes en mi GitHub.

¡Saludos!