Trabajar con los blobs de Azure Storage en PHP

Si estás trabajando con Microsoft Azure, seguro que Azure Storage es uno de tus servicios. Es uno de los más básicos y antiguos de toda la plataforma. Aún recuerdo, a principios del 2010, muchos de los artículos en los que te hablaba de este servicio. ¡Cómo pasa el tiempo!

Imagen de hace 9 años de la plataforma de Azure

En este post te quiero mostrar cómo trabajar con Azure Storage en PHP, más en concreto con los blobs. Lo primero que vamos a hacer es crear una aplicación de prueba con el framework Symfony, que es con el que vengo trabajando. Ejecuta el siguiente comando para crear el esqueleto del proyecto:

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

A día de hoy existen dos librerías, proporcionadas por Microsoft, para trabajar con Azure Storage y PHP: Microsoft Azure Storage PHP Client Libraries y Azure SDK for PHP. Se recomienda que uses la primera de ellas si solo vas a trabajar con Azure Storage, ya que es la que se está actualizando a día de hoy. Sin embargo, no todas los servicios de Azure están disponibles en ella, por lo que si necesitas integrar otros servicios como Azure Service Bus deberás utilizar la segunda.

En Azure Storage PHP Client Libraries, los servicios de blobs, tablas y colas están en librerías separadas. Para este ejemplo, para instalar la de blobs puedes hacerlo a través de composer con el siguiente comando:

composer require microsoft/azure-storage-blob

Encapsula todo lo relativo a Azure Storage en una clase llamada BlobService. Dentro de la carpeta src crea una nueva llamada Service y añade el siguiente código dentro de BlobService.php:

<?php
namespace App\Service;
use MicrosoftAzure\Storage\Blob\BlobRestProxy;
use MicrosoftAzure\Storage\Common\Exceptions\ServiceException;
use Psr\Log\LoggerInterface;
class BlobService
{
    private $logger;
    private $blobClient;
    public function __construct(LoggerInterface $logger)
    {
        $this->logger = $logger;
        $this->blobClient = BlobRestProxy::createBlobService($_SERVER['AZURE_STORAGE_CONNECTION_STRING']);
    }
    public function allContainers()
    {
        try {
            $container_list = $this->blobClient->listContainers();
            return $container_list->getContainers();
        } catch (ServiceException $exception) {
            $this->logger->error('failed to get all containers: ' . $exception->getCode() . ':' . $exception->getMessage());
            throw $exception;
        }
    }
    public function allBlobs($container = 'images')
    {
        try {
            $result = $this->blobClient->listBlobs($container);
            return $result->getBlobs();
        } catch (ServiceException $exception) {
            $this->logger->error('failed to get all blobs: ' . $exception->getCode() . ':' . $exception->getMessage());
            throw $exception;
        }
    }
    public function upload($file, $container = 'images')
    {
        try {
            $content = file_get_contents($file);
            $this->blobClient->createBlockBlob($container, $file->getClientOriginalName(), $content);
        } catch (ServiceException $exception) {
            $this->logger->error('failed to upload the file: ' . $exception->getCode() . ':' . $exception->getMessage());
            throw $exception;
        }
    }
    public function delete($blobName, $container = 'images')
    {
        try {
            $this->blobClient->deleteBlob($container, $blobName);
        } catch (ServiceException $exception) {
            $this->logger->error('failed to delete the file: ' . $exception->getCode() . ':' . $exception->getMessage());
            throw $exception;
        }
    }
}

Como puedes ver, he añadido algunas funciones de ejemplo como recuperar los contenedores de una cuenta, los blobs, subir un archivo o eliminarlo. Obviamente, hay muchas otras acciones que se pueden realizar pero es para que tengas una idea de su estructura y lo que necesitas para conectarte a una cuenta de Azure Storage. Como ves, en el constructor se crea el cliente para poder conectarte. Debes incluir en el archivo .env el valor para la clave AZURE_STORAGE_CONNECTION_STRING, la cual debe tener este formato:

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

Ahora crea un nuevo controlador llamado BlobsController. Puedes ayudarte de este comando para generarlo:

php bin/console make:controller BlobsController

Este comando te genera tanto la clase para el controlador como una página asociada al mismo. Reemplaza el código de BlobsController.php por el siguiente:

<?php
namespace App\Controller;
use App\Service\BlobService;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
class BlobsController extends AbstractController
{
    /**
     * @Route("/", name="blobs")
     */
    public function index(BlobService $storage)
    {
        return $this->render('blobs/index.html.twig', [
            'controller_name' => 'BlobsController',
            'blobs' => $storage->allBlobs(),
            'containers' => $storage->allContainers(),
        ]);
    }
    /**
     * @Route("/upload/image", methods={"POST"})
     */
    public function create(Request $request, BlobService $storage)
    {
        $file = $request->files->get('newFile');
        if (empty($file)) {
            return new Response("No file specified", Response::HTTP_UNPROCESSABLE_ENTITY, ['content-type' => 'text/plain']);
        }
        $storage->upload($file);
        return $this->redirectToRoute('blobs');
    }
    /**
     * @Route("/delete/{blobName}")
     */
    public function deleteBlob($blobName, BlobService $storage)
    {
        $storage->delete($blobName);
        return $this->redirectToRoute('blobs');
    }
}

Para poder ver el resultado he modificado el archivo templates/base.html.twig para añadir 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>

Por último he modificado templates/blobs/index.html.twig para mostrar el listado de blobs que tengo en el container images:

{% extends 'base.html.twig' %}
{% block title %}Azure Blob storage{% endblock %}
{% block body %}
<div class="row">
    <div class="col-lg-12 jumbotron">
        <h1>Azure Blob storage</h1>
        <p class="lead">It's Microsoft's object storage solution for the cloud. Blob storage is optimized for storing massive amounts of unstructured data. Unstructured data is data that does not adhere to a particular data model or definition, such as text or binary data.</p>
    </div>
</div>
<div class="row">
    <div class="col">        
        <form action="/upload/image" enctype="multipart/form-data" method="post" class="form-inline">
           <div class="input-group mb-3">                
                <div class="custom-file">
                    <input type="file" class="custom-file-input" id="newFile" name="newFile" aria-describedby="inputGroupFileAddon01">
                    <label class="custom-file-label" for="newFile">Choose file to upload</label>
                </div>
                <div class="input-group-append">                    
                    <button type="submit" class="btn btn-primary">Upload</button>
                </div>
            </div>            
        </form>        
    </div>
    <div>
    <div class="col">
            <div class="dropdown">
                <button class="btn btn-secondary dropdown-toggle" type="button" id="dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
                    Containers
                </button>
                <div class="dropdown-menu " aria-labelledby="dropdownMenuButton" >                
                    {% for container in containers %}
                    <a class="dropdown-item" href="#">{{container.name}}</a>
                    {% endfor %}
                </div>
            </div>        
        </div>
    </div>
</div>
<div class="row">
        {% for blob in blobs %}
            <div class="col-sm">
                <div class="card" style="width: 15rem; margin-bottom:10px;">
                    <img src="{{ blob.url }}" class="card-img-top" alt="{{blob.name}}">
                    <div class="card-body">
                        <h5 class="card-title">{{blob.name}}</h5>
                        <p class="card-text">Some quick example text to build on the card title and make up the bulk of the card's content.</p>
                        <a href="/delete/{{blob.name}}" class="btn btn-primary">Delete</a>
                    </div>
                </div>
            </div>    
        {% endfor %}   
</div>
{% endblock %}

El resultado debería de ser parecido al siguiente, si has elegido los personajes de Breaking Bad como imágenes claro 🙂

Ejemplo de Azure Storage Blobs en PHP
Ejemplo de Azure Storage Blobs en PHP

El ejemplo completo lo tienes en GitHub.

¡Saludos!