Etiqueta: Frameworks

  • Cuál es el mejor framework PHP para hacer APIs REST

    Te encomendaron hacer una nueva API REST para tu aplicación PHP.

    Podrías hacer en PHP puro, claro que sí pero tendrías que ponerte a tratar con una cantidad de cosas bastante molestas, entre ellas:

    • El ruteo
    • Los diferentes métodos HTTP
    • La autenticación/autorización
    • El caché
    • La documentación

    Qué pereza, ¿no? Al fin de cuentas… una API es una API… tiene que haber alguna herramienta que simplifique esto.

    Claro que la hay.

    Hace unos años te habría recomendado sin pensarlo dos veces usar Tonic (De hecho, yo mismo lo usé para armar un proyecto bastante lindo de procesamiento de imágenes).

    Pero… como todo en esta vida, la tecnología evoluciona y, para qué usar una herramienta menos potente cuando podríamos usar una más moderna, ¿no?.

    Salvo que tengas muchas ganas de aprender algunas cosas y prefieras hacerlo todo a pulmón como hice yo en este repositorio (Bueno, en rigor de verdad no empecé de 0, usé unas cuantas librerías de Symfony y alguna otra cosita como Swagger-PHP para generar la documentación de OpenAPI pero aún así… le puse algunas horas de coding).

    Ahora bien, si lo tuyo es ir directo a lo rápido y seguro no hay muchas dudas: API-Platform es lo que buscás.

    Se trata de un framework basado en lo mejorcito que hay ahí afuera, se trata de definir tus modelos y… no mucho más.

    Una API crocante en 10′.

    Vamos por pasos:

    Instalar el framework

    Bueno… un pequeño pasito antes es instalar Docker y docker-compose, pero seguramente ya lo hiciste.

    Vamos al framework

    gh repo create --clone --template api-platform/api-platform my-api --public
    cd my-api
    
    docker compose up -d --wait
    open https://localhost

    No te preocupes si ve un aviso algo tenebroso como este:

    Nadie te está por robar todo el dinero de tu cuenta, simplemente se trata de que estás usando https para entrar a tu propio localhost y el browser no acepta el certificado autofirmado.

    Si querés seguir podés hacerlo con tranquilidad. Si no, podés buscar ayuda para configurar certificados auto-firmados.

    En cualquier caso, deberías terminar viendo algo como:

    No está tan mal para haber requerido 4 comandos, ¿no?

    Definir tus recursos

    Para empezar con algo simple, definí tus entidades dentro del directorio api/src/Entity. Por ejemplo, esta es una de las que yo usé en mi proyecto:

    <?php
    
    namespace App\Entity;
    
    use App\Repository\FactionRepository;
    use Doctrine\Common\Collections\ArrayCollection;
    use Doctrine\Common\Collections\Collection;
    use Doctrine\DBAL\Types\Types;
    use Doctrine\ORM\Mapping as ORM;
    
    #[ORM\Entity(repositoryClass: FactionRepository::class)]
    #[ORM\Table(name: '`factions`')]
    class Faction
    {
        #[ORM\Id]
        #[ORM\GeneratedValue]
        #[ORM\Column]
        private ?int $id = null;
    
        #[ORM\Column(length: 128)]
        private ?string $faction_name = null;
    
        #[ORM\Column(type: Types::TEXT)]
        private ?string $description = null;
    
        #[ORM\OneToMany(targetEntity: Character::class, mappedBy: 'faction')]
        private Collection $characters;
    
        public function __construct(string $faction_name, string $description, ?int $id = null)
        {
            $this->id = $id;
            $this->faction_name = $faction_name;
            $this->description = $description;
            $this->characters = new ArrayCollection();
        }
    
        public function getId(): ?int
        {
            return $this->id;
        }
    
        public function getFactionName(): ?string
        {
            return $this->faction_name;
        }
    
        public function setFactionName(string $faction_name): static
        {
            $this->faction_name = $faction_name;
    
            return $this;
        }
    
        public function getDescription(): ?string
        {
            return $this->description;
        }
    
        public function setDescription(string $description): static
        {
            $this->description = $description;
    
            return $this;
        }
    
        /**
         * @return Collection<int, Character>
         */
        public function getCharacters(): Collection
        {
            return $this->characters;
        }
    
        public function addCharacter(Character $character): static
        {
            if (!$this->characters->contains($character)) {
                $this->characters->add($character);
                $character->setFaction($this);
            }
    
            return $this;
        }
    
        public function removeCharacter(Character $character): static
        {
            if ($this->characters->removeElement($character)) {
                // set the owning side to null (unless already changed)
                if ($character->getFaction() === $this) {
                    $character->setFaction(null);
                }
            }
    
            return $this;
        }
    
        public function __toString(): string
        {
            return $this->faction_name;
        }
    }

    En este caso estoy usando Doctrine pero existen muchas otras opciones.

    Hasta acá el modelo.

    Si vas a https://localhost/docs vas a ver algo como:

    Que tampoco está tan mal, ¿no? Una interface Swagger interactiva que describe todas las operaciones disponibles en tu API.

    ¿Que por qué dice «greetings»? Ah, sí… detallín. Greetings es el modelo dummy que viene con el ejemplo.

    Probablemente lo que esperabas era algo más del estilo de:

    Lograrlo es bastante fácil, se trata de agregar esta etiqueta al comienzo de la definición de la clase Faction:

    #[ApiResource]

    Luego re-cargar y… voilà. La API está lista… o casi.

    Actualizar la base de datos

    Para poder ingresar/leer datos no sólo se necesita el código PHP, la base de datos también debe estar actualizada.

    Usando estos comandos:

    docker compose exec php php bin/console make:migration

    Y

    docker compose exec php php bin/console doctrine:migrations:migrate -q

    Estará todo listo para probar la API.

    Con el comando:

    curl -X 'POST' \
      'https://localhost/factions' \
      -H 'accept: application/ld+json' \
      -H 'Content-Type: application/ld+json' \
      -d '{  
      "faction_name": "A Faction",
      "description": "A description"
    }'

    Obtendrás esto como respuesta:

    {
      "@context": "/contexts/Faction",
      "@id": "/factions/1",
      "@type": "Faction",
      "id": 1,
      "faction_name": "A Faction",
      "description": "A description",
      "characters": [],
      "factionName": "A Faction"
    }

    Y también unos cuantos encabezados interesantes:

     accept-patch: application/merge-patch+json 
     alt-svc: h3=":443"; ma=2592000 
     cache-control: no-cache,private 
     content-location: /factions/1 
     content-type: application/ld+json; charset=utf-8 
     date: Thu,17 Oct 2024 18:01:30 GMT 
     link: <https://localhost/docs.jsonld>; rel="http://www.w3.org/ns/hydra/core#apiDocumentation" 
     location: /factions/1 
     permissions-policy: browsing-topics=() 
     server: Caddy 
     vary: Accept 
     x-content-type-options: nosniff 
     x-debug-token: 0d76d9 
     x-debug-token-link: https://localhost/_profiler/0d76d9 
     x-frame-options: deny 
     x-robots-tag: noindex 

    Todo esto también lo podés ver más cómodamente desde acá:

    Y por supuesto, podés probar los otros métodos HTTP.

    Agregar lógica de negocio

    ¿Validaciones dijiste? Ningún problema, hagamos algunas.

    ¿Qué tal una que no permita dejar vacío el nombre ni la descripción de la Faction?

    Symfony al rescate: usá el atributo Symfony\Component\Validator\Constraints\NotBlank y a otra cosa:

    #[ORM\Entity(repositoryClass: FactionRepository::class)]
    #[ORM\Table(name: '`factions`')]
    #[ApiResource]
    class Faction
    {
        #[ORM\Id]
        #[ORM\GeneratedValue]
        #[ORM\Column]
        private ?int $id = null;
    
        #[ORM\Column(length: 128)]
        #[NotBlank]
        private ?string $faction_name = null;
    
        #[ORM\Column(type: Types::TEXT)]
        #[NotBlank]
        private ?string $description = null;

    Si probás ejecutar:

    curl -X 'POST' \ 'https://localhost/factions' \ -H 'accept: application/ld+json' \ -H 'Content-Type: application/ld+json' \ -d '{ "faction_name": "", "description": "Not empty" }'

    Vas a obtener:

    {
      "@id": "/validation_errors/c1051bb4-d103-4f74-8988-acbcafc7fdc3",
      "@type": "ConstraintViolationList",
      "status": 422,
      "violations": [
        {
          "propertyPath": "faction_name",
          "message": "This value should not be blank.",
          "code": "c1051bb4-d103-4f74-8988-acbcafc7fdc3"
        }
      ],
      "detail": "faction_name: This value should not be blank.",
      "hydra:title": "An error occurred",
      "hydra:description": "faction_name: This value should not be blank.",
      "type": "/validation_errors/c1051bb4-d103-4f74-8988-acbcafc7fdc3",
      "title": "An error occurred"
    }

    Control de acceso

    El control de acceso se puede implementar de forma muy sencilla también: a través de la propiedad security del atributo #[ApiResource].

    Basta con cambiar el encabezado por:

    #[ApiResource(security: "is_granted('ROLE_USER')")]
    class Faction

    Para que, al hacer esta petición:

    curl -X 'GET' \
      'https://localhost/factions?page=1' \
      -H 'accept: application/ld+json'

    La respuesta sea:

    {
      "@id": "/errors/401",
      "@type": "hydra:Error",
      "title": "An error occurred",
      "detail": "Full authentication is required to access this resource.",
      "status": 401,
      "type": "/errors/401",
      "trace": [
        {
          "file": "/app/vendor/symfony/security-http/Firewall/ExceptionListener.php",
          "line": 189,
          "function": "throwUnauthorizedException",
          "class": "Symfony\\Component\\Security\\Http\\Firewall\\ExceptionListener",
          "type": "->"
        },
        {
          "file": "/app/vendor/symfony/security-http/Firewall/ExceptionListener.php",
          "line": 148,
          "function": "startAuthentication",
          "class": "Symfony\\Component\\Security\\Http\\Firewall\\ExceptionListener",
          "type": "->"
        },
        {
          "file": "/app/vendor/symfony/security-http/Firewall/ExceptionListener.php",
          "line": 103,
          "function": "handleAccessDeniedException",
          "class": "Symfony\\Component\\Security\\Http\\Firewall\\ExceptionListener",
          "type": "->"
        },
        {
          "file": "/app/vendor/symfony/event-dispatcher/Debug/WrappedListener.php",
          "line": 116,
          "function": "onKernelException",
          "class": "Symfony\\Component\\Security\\Http\\Firewall\\ExceptionListener",
          "type": "->"
        },
        {
          "file": "/app/vendor/symfony/event-dispatcher/EventDispatcher.php",
          "line": 220,
          "function": "__invoke",
          "class": "Symfony\\Component\\EventDispatcher\\Debug\\WrappedListener",
          "type": "->"
        },
        {
          "file": "/app/vendor/symfony/event-dispatcher/EventDispatcher.php",
          "line": 56,
          "function": "callListeners",
          "class": "Symfony\\Component\\EventDispatcher\\EventDispatcher",
          "type": "->"
        },
        {
          "file": "/app/vendor/symfony/event-dispatcher/Debug/TraceableEventDispatcher.php",
          "line": 139,
          "function": "dispatch",
          "class": "Symfony\\Component\\EventDispatcher\\EventDispatcher",
          "type": "->"
        },
        {
          "file": "/app/vendor/symfony/http-kernel/HttpKernel.php",
          "line": 239,
          "function": "dispatch",
          "class": "Symfony\\Component\\EventDispatcher\\Debug\\TraceableEventDispatcher",
          "type": "->"
        },
        {
          "file": "/app/vendor/symfony/http-kernel/HttpKernel.php",
          "line": 91,
          "function": "handleThrowable",
          "class": "Symfony\\Component\\HttpKernel\\HttpKernel",
          "type": "->"
        },
        {
          "file": "/app/vendor/symfony/http-kernel/Kernel.php",
          "line": 197,
          "function": "handle",
          "class": "Symfony\\Component\\HttpKernel\\HttpKernel",
          "type": "->"
        },
        {
          "file": "/app/vendor/symfony/runtime/Runner/Symfony/HttpKernelRunner.php",
          "line": 35,
          "function": "handle",
          "class": "Symfony\\Component\\HttpKernel\\Kernel",
          "type": "->"
        },
        {
          "file": "/app/vendor/autoload_runtime.php",
          "line": 29,
          "function": "run",
          "class": "Symfony\\Component\\Runtime\\Runner\\Symfony\\HttpKernelRunner",
          "type": "->"
        },
        {
          "file": "/app/public/index.php",
          "line": 5,
          "function": "require_once"
        }
      ],
      "hydra:title": "An error occurred",
      "hydra:description": "Full authentication is required to access this resource."
    }

    Desplegar en Producción

    A la hora de desplegar la recomendación es usar el mismo docker compose que trae el framework. ¿Tenés dudas sobre este punto? Acá tenés un recurso que puede ser interesante.

    Pues aquí bastará con tener algún servidor donde desplegar (Mi recomendación aquí es usar un sencillo Droplet de DigitalOcean), instalarle docker y crear un contexto especial, de modo de terminar lanzando un comando tipo:

    docker compose -c prod docker-compose.yml up -d --wait

    Ah! Casi olvido un detalle importante: modificar el archivo .env para que diga APP_ENV=prod en lugar de APP_ENV=dev.

    Y listo. Bueno, luego vendrá, si corresponde, mover el DNS y demás pero eso será igual se trate o no de una API.

    Qué más se puede hacer con API-Platform

    La verdad es que mucho, pero no puedo meter todo en un único post.

    Dejo algunas cosas que me parecen sumamente interesantes para que las investigues:

    • Scaffolding de clientes
    • Comunicación en tiempo real a través de Mercure
    • Generación de los modelos a partir de especificaciones OpenApi

    ¿Te convencí? Dejame tus preguntas en los comentarios.

  • ¿Debo aprender php desde cero o ir directo con un framework?

    ¿Debo aprender php desde cero o ir directo con un framework?

    Una pregunta que se hace mucha gente que está queriendo empezar con PHP es, habiendo tantos frameworks y habiendo escuchado cosas tan buenas de los frameworks, ¿vale la pena aprender PHP «a secas»?

    Qué es un framework

    Empecemos por la definición para entender de qué estamos hablando: un framework es, ante todo, un conjunto de código que ya está escrito y que resuelve una cantidad de problemas genéricos (problemas que muchas aplicaciones diferentes deben resolver).

    En definitiva, un framework es un punto de partida muy bueno para una aplicación de cierta complejidad, podrías pensarlo como un atajo.

    Cómo está hecho un framework

    Salvo casos muy excepcionales (como Phalcon), los frameworks están escritos usando el mismo lenguaje que los programas «finales». En el caso de aplicaciones PHP, los frameworks son, técnicamente, código PHP.

    De modo que, para comprender qué es lo que hace un framework necesitas conocer el lenguaje.

    ¿Es necesario conocer PHP para usar un framework PHP?

    Pregunta difícil de responder  :).

    Yo diría que sí. Aunque sea muy por encima, se necesitan algunas nociones de Programación Orientada a Objetos (y específicamente de PHP para comprender la sintaxis).

    Digamos que si entendés lo que significa esto:

    <?php
    
    use MiFramework\App;
    
    $app = new App();
    $app->run();

    Sabés lo suficiente como para empezar a aprender a usar un framework.

    ¿Es conveniente conocer PHP para usar un framework PHP?

    Siempre es bueno conocer las bases de las herramientas que uno usa.

    Yo, por ejemplo, empecé estudiando C y C++ y, aunque ahora ya no los uso, conocer por dentro cómo funcionan lenguajes de más bajo nivel me permitió tener una comprensión mucho mayor de qué es lo que hago y, de esa forma, aprovechar mejor los recursos a mi alcance.

    Particularmente, cuando se trata de PHP, saber hacer las cosas sin framework te permite:

    • Avanzar ahí donde el framework elegido puede no ser ideal (o tus conocimientos sobre él no son suficientes)
    • Meterte en el código del framework (no necesariamente para hacer mejoras, pero muchas veces identificar errores es más simple si podés hacer esto)
    • Conocer nuevos modos de resolver problemas (Mediante una herramienta como xdebug podés seguir el código mientras se ejecuta e ir analizándolo)

    Conclusión

    Según el nivel en que te encuentres actualmente te recomendaría:

    • Si conocés PHP pero no dominás por completo la Programación Orientada a Objetos te conviene adquirir esos conocimientos (Este libro puede ayudarte)
    • Si ya estás familiarizado con la Programación Orientada a Objetos en PHP tenés todo lo necesario para sumergirte en un framework (Este curso puede ayudarte) 
  • ¿Usar un framework perjudica el SEO?

    ¿Usar un framework perjudica el SEO?

    Ah… el SEO… ese arte indómito de la web :).

    Mucho se habla de SEO (Search Engine Optimization) y muy poco es lo que realmente se sabe. Sin ser un experto en la materia (de hecho, como buen desarrollador, muy lejos estoy de serlo :p), tengo mis opiniones al respecto.

    Más allá de qué es el SEO o mejor dicho, cómo operar en favor del SEO de un sitio, todo el mundo sabe que el SEO es algo bueno y, por lo tanto, cualquier cosa que lo perjudique es algo malo, ¿cierto?… No tan rápido :).

    El SEO forma parte de una estrategia de adquisición de tráfico, un tema de Marketing Online que, dado que este no es un espacio para discutir sobre Marketing Online, está fuera de alcance.

    A lo que sí quiero hacer referencia es a esta pregunta que vengo leyendo en varios lugares (Dicha con más o menos palabras):

    ¿Es mejor para el SEO de un sitio desarrollarlo usando php puro en lugar de algún framework standard?

    La verdad… no entiendo bien cómo se llega a esa conclusión, pero bueno… intentaré dar una respuesta.

    De qué se trata SEO

    Sin entrar en muchos detalles, SEO se trata de disponer los contenidos y la estructura de un sitio de modo de facilitar a los motores de búsqueda (Google, Bing, etc…) la incorporación de un sitio a sus índices (Y de ese modo lograr que aparezca dentro de los resultados de búsqueda de los usuarios que queremos atraer… esperablemente en las primeras páginas).

    Los factores que toman en cuenta los motores de búsqueda para decidir en qué posición colocan a cada sitio son muy variados y cambiantes constantemente (De eso viven los consultores SEO). Sin embargo, hay algunos que tienen especial relevancia, entre ellos, la velocidad de carga del sitio (y supongo que por este lado viene la pregunta).

    Cómo afecta un framework a la velocidad de carga de un sitio

    Es muy difícil hablar de frameworks como un genérico. Existen algunos que están especialmente orientados a manejar sitios de alto tráfico (y, por ende, a hacer un uso muy eficiente de los recursos), otros a generar código más sencillo de mantener (aún a costa de algo de performance).

    Personalmente, desconfío de las ventajas de no usar un framework. En mi experiencia, siempre es más fácil optimizar algo que está bien estructurado que lo contrario.

    De hecho, las mayores optimizaciones en lo que a performance se refieren vienen por el lado de la infraestructura más que del código (Por ejemplo, implementando un web server potente como NginX, poniendo más servidores, usando balanceadores de carga, cachés como Varnish y otra cantidad de truquitos por el estilo).

    Existe una visión bastante arraigada (especialmente entre los programadores de nivel intermedio) de que los frameworks son mastodontes pesados que agregan una capa de complejidad y features muy probablemente innecesarios.

    Es posible que así haya sido anteriormente (aunque lo dudo), pero la realidad de hoy es muy diferente. Los frameworks de PHP están sumamente atomizados (Es decir, al crear una aplicación podés combinar las partes que te resulten útiles sin sobrecargar tu aplicación).

    Por otro lado, ¡generar código optimizado no es una tarea sencilla!

    Programando para la web estamos lejos de los sistemas hechos en Assembler donde cada ciclo de reloj cuenta y nada garantiza que no usar un framework estándar genere mejor código que sí hacerlo.

    Lo que sí garantiza no usar un framework estándar es tener que trabajar en resolver problemas que ya están resueltos (Lo que se dice reinvetar la rueda).

    ¿Puede un framework estándar ayudar al SEO?

    De hecho sí, puede y bastante. Una muestra muy simple es el tema de las URLs amigables (Otro de los pocos «trucos» de SEO que conozco).

    Hace un tiempo que a los motores de búsqueda les parece mejor una URL del tipo http://misito.com/un-tema-muy-popular que una del tipo http://misito.com/index.php?post_id=1271.

    Generar este tipo de URLs a partir del contenido de una página no es una tarea trivial (Puede ser simple, pero mejor si alguien ya lo hizo, ¿no?) y después está el tema complementario… cómo llegar desde la URL http://misito.com/un-tema-muy-popular al post con id 1271.

    De nuevo, no es la muerte de nadie armar este mecanismo, pero si un framework ya lo tiene hecho… ¿quién paga esas horas?

    Conclusión

    En última instancia, siempre se trata de una cuestión de comodidad personal, pero, aún cuando decidas no usar un framework, es importante poder tomar esa decisión con fundamentos y la preocupación por el SEO no es uno bueno.

  • Un redimensionador de imágenes eficiente hecho con PHP

    Un redimensionador de imágenes eficiente hecho con PHP

    Un proyecto interesante que tuve la oportunidad de realizar hace unos años fue un sistema de procesamiento de imágenes.

    El desafío era lograr un servicio simple que permitiera escalar y rotar imágenes velozmente.

    Lo diseñé como un componente separado de la aplicación principal (Una red social de viajes) para poder instalarlo sin inconvenientes en un servidor diferente (y eventualmente poder vincularlo a otros proyectos).

    Por entonces me pareció una buena idea montarlo sobre una arquitectura RESTFul y, como era un proyecto muy simple y acotado (y tenía ganas de aprender algo nuevo de paso) decidí usar un framework especialmente diseñado para estos efectos: Tonic.

    Lo más importante como siempre: ¿qué nombre ponerle a una aplicación como esta? Mucho de fotografía no sé, pero buscando un poco me pareció que Bresson podía servir 🙂

    Muy bien, teniendo despejado el terreno había que empezar a trabajar.

    Para la parte dura del desarrollo no había mucho que pensar: ImageMagick iba a ser el motor de procesamiento.

    Las URIs tenían que ser simples y a la vez semánticas. Un caso de uso muy común era requerir una cierta imagen en un tamaño diferente al original, ¿qué mejor que usar el clasico WWxHH (Ancho por alto) como sufijo?

    Para lograr la eficiencia buscada (además de tener un buen soporte de hardware) había que reducir al máximo las transformaciones de las imágenes… basta con almacenar cada imagen transformada para próximos usos… claro que si se trata de un sistema que maneja miles y miles de imágenes el almacenamiento puede volverse algo problemático (Menos mal que contábamos con Amazon S3).

    Y por último, como para darle un poco más de robustez, me pareció interesante hacerlo agnóstico respecto del medio de almacenamiento y del motor de manipulación de imágenes… al fin y al cabo, uno nunca sabe lo que depara el destino y la verdad… no costaba mucho hacerlo un poco más elegante.

    Y bueno… así nació este pequeño proyecto.

    ¿Querés ver código? Acá vamos 🙂

    En este caso sólo hay disponible un tipo de recurso (Resource): la imagen. Para eso tenemos la clase ImageResource.

    namespace Bresson;
    
    use Tonic\Resource;
    use Tonic\Exception;
    use Tonic\Response;
    
    /**
     * @uri /(.+)-(\d+)x(\d+)
     * @uri /(.+)-(\d+)x(\d+)-r(\d+)
     * @uri /(.+)-(th)
     * @uri /(.+)-(bg)
     * @uri /(.+)
     */
    
    class ImageResource extends Resource
    {
        const MAX_RETRIES = 20;
    
        /**
         * @method get
         * @provides image/jpeg
         */
        public function get($prefix, $width = null, $height = null, $degrees = null)
        {
            $dataSource = $this->app->container['DS'];
            $sOriginalRequest = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
            $this->app->container['logger']->addDebug('Buscando '.$sOriginalRequest);
            if(empty($height) && empty($width)){
                $original = true;
            } else {
                $original = false;
            }
            if ( $dataSource->elementExists($sOriginalRequest) && $contents = $dataSource->fetchElement($sOriginalRequest,$original) ) {
                $this->app->container['logger']->addInfo( $sOriginalRequest. ' encontrado en el storage');
                $this->app->container['logger']->addDebug('Contenido obtenido');
    
                return new Response(
                    Response::OK,
                    $contents,
                    array(
                        'Content-Length' => strlen($contents),
                    ));
            } else {
                $this->app->container['logger']->addInfo($sOriginalRequest.' NO encontrado en el storage');
                if ( !empty($width) && !$original) {
                    $sOriginalName = '/'.$prefix.substr( $sOriginalRequest, strrpos( $sOriginalRequest, '.' ) );
                    
                    $this->app->container['logger']->addDebug('Request contiene sufijo, buscando imagen original', array( 'imagenOriginal' => $sOriginalName ) );
                    if ( !$dataSource->elementExists( $sOriginalName ) ) {
                        $this->app->container['logger']->addDebug('Archivo original no encontrado', array( 'Archivo' => $sOriginalName ) );
                        $sOriginalName = '/'.$prefix.'-bg'.substr( $sOriginalRequest, strrpos( $sOriginalRequest, '.' ) );
                        if ( !$dataSource->elementExists( $sOriginalName ) ) {
                            $this->app->container['logger']->addDebug('Archivo bg no encontrado', array( 'Archivo' => $sOriginalName ) );
                            $sOriginalName = '/'.$prefix.'-th'.substr( $sOriginalRequest, strrpos( $sOriginalRequest, '.' ) );
                            if ( !$dataSource->elementExists( $sOriginalName ) ) {
                                $this->app->container['logger']->addDebug('Archivo th no encontrado', array( 'Archivo' => $sOriginalName ) );
    
                                throw new \Tonic\NotFoundException;                 
                            }
                        }
                    }
                    $this->app->container['logger']->addDebug('Imagen original encontrada en el storage', array( 'imagenOriginal' => $sOriginalName ) );
                    try {
                        $sLockFileName = '/tmp/'.preg_replace( '|/|', '.', $sOriginalRequest ) .'.lck';
                        if ( @$fp = fopen( $sLockFileName, 'x' ) ) {
                            $this->app->container['logger']->addDebug('Lock obtenido', array( 'lockFile' => $sLockFileName ) );
                            $processor = $this->app->container['imageProcessor'];
                            $tmpFile = tempnam(__DIR__, 'img');
                            $this->app->container['logger']->addDebug('Descargando archivo original a temporal', array( 'tmpFile' => $tmpFile ) );
                            file_put_contents( $tmpFile, $dataSource->fetchElement($sOriginalName,true) );
                            $processor->readImageFile( $tmpFile );
    
                            $this->app->container['logger']->addInfo('Archivo original obtenido, procesando imagen');
    
                            if ($degrees) {
                                $processor->rotate($degrees);
                            }
    
                            if ( !is_numeric($width) ) {
                                $this->app->container['logger']->addDebug('El ancho no es numérico, se busca una regla de redimensión', array( 'width' => $width ) );
                                $aParts = explode("/", $sOriginalName);
                                $taxonomy = $aParts[1];
                                if ( !array_key_exists($taxonomy, $this->app->container['config']['process_rules'] ) ) {
                                    $this->app->container['logger']->addError('Se pidio una taxonomía desconocida', array( 'taxonomy' => $taxonomy ) );
    
                                    throw new \Tonic\Exception( 'Unknown taxonomy "'.$taxonomy.'"', 400 );
                                }
    
                                $rules = $this->app->container['config']['process_rules'][$taxonomy];
    
                                if ( !array_key_exists( $width, $rules ) ) {
                                    $this->app->container['logger']->addError('Se pidio un tamaño desconocido para la taxonomia', array( 'taxonomy' => $taxonomy, 'width' => $width ) );
    
                                    throw new \Tonic\Exception( 'Unknown size "'.$width.'", available sizes for "'.$taxonomy.'": ['.implode(', ', array_keys($rules)).']', 400 );
                                }
    
                                $size = $rules[$width];
                                list( $width, $height ) = getimagesize($tmpFile);
    
                                if($width > $height){
                                    $width = $size;
                                    $height = 0;
                                } else {
                                    $width = 0;
                                    $height = $size;
                                }
    
                                if ( !$processor->scale( $width, $height ) ) {
                                    $this->app->container['logger']->addCritical('No se pudo hacer el resizing' );
    
                                    throw new \Tonic\Exception( 'Image couldn\'t be scaled', 500 );
                                }
                            } else {
                                $result = $processor->scale( $width, $height );
                                if ( !$result ) {
    
                                    $this->app->container['logger']->addCritical('No se pudo hacer el resizing' );
    
                                    throw new \Tonic\Exception( 'Image couldn\'t be resized', 500 );
                                }
    
                                $processor->centerCrop( $width, $height);
                            }
    
                            $processor->writeImageFile( $tmpFile );
                            $contents = file_get_contents($tmpFile);
                            unlink($tmpFile);
    
                            $this->app->container['logger']->addDebug('Almacenando en el storage' );
                            /**
                             * @todo Agregar manejo de exception y logging (En todo caso, si no se puede guardar que devuvelva y vuelva a calcular en el próximo request
                             */
                            $dataSource->storeElement( $sOriginalRequest, $contents );
    
                            $this->app->container['logger']->addDebug('Liberando el lock' );
                            fclose($fp);
                            unlink($sLockFileName);
                        } else {
                            $this->app->container['logger']->addDebug('No se pudo obtener el lock, otro proceso debe estar generando' );
                            $tries = 0;
                            do {
                                usleep( 10 );
                                $tries++;
                                $elementExists = $dataSource->elementExists($sOriginalRequest);
                            } while ( !$elementExists && $tries < ImageResource::MAX_RETRIES );
    
                            if ( $elementExists ) {
                                $this->app->container['logger']->addDebug('Otro proceso generó el archivo buscado' );
                                $contents = $dataSource->fetchElement($sOriginalRequest);
                            } else {
                                $this->app->container['logger']->addCritical('Se agotaron los reintentos' );
    
                                throw new \Tonic\NotFoundException;
                            }
                        }
    
                        return new Response(
                            Response::OK,
                            $contents,
                            array(
                                'Content-Length' => strlen($contents),
                            ));
                    } catch ( Exception $e ) {
                        $this->app->container['logger']->addCritical('Exception inesperada: ', array( 'exception' => $e->getMessage() ) );
    
                        throw new Exception( 500, $e->getMessage() );
                    }
                } else {
                    $this->app->container['logger']->addDebug('Archivo no encontrado',array('Archivo' => $sOriginalRequest) );
    
                    throw new \Tonic\NotFoundException;
                }
            }
        }
    }

    Las partes interesantes:

    /**
     * @uri /(.+)-(\d+)x(\d+)
     * @uri /(.+)-(\d+)x(\d+)-r(\d+)
     * @uri /(.+)-(th)
     * @uri /(.+)-(bg)
     * @uri /(.+)
     */
    

    Esta es la forma de mapear una URI a un controlador en Tonic… no está mal, ¿cierto?

    La primera regla es la más cómun: imagen-ANCHOxALTO, simplemente redimensiona.

    La segunda hace lo mismo más una rotación: imagen-ANCHOxALTO-rGRADOS.

    La tercera y la cuarta (th y bg) simplemente son tamaños predeterminados (Algo que tenía mucho sentido en la aplicación cliente).

    Por último, si se quiere una imagen sin alteración alguna también hay que proveerla…

    /**
     * @method get
     * @provides image/jpeg
     */

    Una forma muy simple de definir el método HTTP aceptado y el Content-Type retornado.

    $dataSource = $this->app->container['DS'];

    La parte del agnosticismo respecto del medio de almacenamiento se logra a través de esta dependencia (El DataSource), más sobre esto más abajo.

    $sLockFileName = '/tmp/'.preg_replace( '|/|', '.', $sOriginalRequest ) .'.lck';
    if ( @$fp = fopen( $sLockFileName, 'x' ) ) {

    Todas las partes del código que hacen referencia al LockFile son simplemente un mecanismo de exclusión mutua. Tené en cuenta que en este contexto era muy frecuente tener una gran cantidad de usuarios solicitando la misma imagen a la vez, y una de las premisas del proyecto era ser eficiente… no podíamos permitirnos procesar una misma imagen más de una vez, con lo cual, tuvimos que implementar este mecanismo para asegurarnos de no saturar los recursos del servidor en tareas redundantes (Ok, supongo que podríamos haber usado algo de más bajo nivel como los semáforos… en aquel momento no los conocía).

    $processor = $this->app->container['imageProcessor'];

    A través de esta dependencia logramos agnosticismo respecto del motor de procesamiento de imágenes.

    Y lo demás se explica por sí mismo 🙂

    Lo importante de notar en este código es que se trata de un thin controller, no hace mucho más que preparar el terreno para que el procesador de imágenes y el data source hagan su trabajo cómodamente.

    La inyección de las dependencias se realiza desde el entry point principal de la aplicación: web/dispatch.php

    $app = new Tonic\Application($config);
    $app->container = new Pimple();
    

    Apenas creada la aplicación se le agrega un simple contenedor de dependencias (Pimple) y luego se van generando las dependencias en base a una configuración:

    $bressonConfig = array(
       'process_rules' => array(),
       'data_source' => array(
          'class' => 'Bresson\LocalStorage',
          'init_params' => array(
             'base_dir' => __DIR__.DIRECTORY_SEPARATOR.'..'.DIRECTORY_SEPARATOR.'files',
          ),
       ),
       'image_processor' => array(
          'class' => 'Bresson\IMagickProcessor',
       ),
       'log' => array(
          'filename' => __DIR__.'/../log/bresson.log',
          'level' => 'DEBUG',
       ),
    );
    $app->container['config'] = $bressonConfig;
    $app->container['DS'] = new $app->container['config']['data_source']['class']( $app->container['config']['data_source']['init_params'] );
    $app->container['imageProcessor'] = new $app->container['config']['image_processor']['class']();

    Por último tenemos dos interfaces que nos permiten abstraer las clases específicas que realizan las tareas complejas:

    • ImageProcessor
    • Storage

    En este caso sólo tenemos un ImageProcessor: IMagickProcessor el cual en los hechos no es más que un wrapper para la clase Imagick.

    En cambio, del lado de los Storage tenemos dos posibilidades:

    • LocalStorage
    • S3Storage

    El primero es muy simple: utiliza como medio de almacenamiento el sistema de archivos local, el segundo es algo más interesante: usa S3.

    Pero lo importante es que, dada la arquitectura de la solución, implementar nuevos soportes de almacenamiento o procesadores de imágenes se vuelve algo super sencillo:

    Basta con crear una clase que implemente la interfase adecuada (Sea ImageProcessor o Storage) y modificar una pequeña configuración (Que, dicho sea de paso, debería estar en un archivo separado…).

    Y listo, tenemos un pequeño pero potente procesador de imágenes muy extendible y escalable.

    ¿Qué lecciones te llevás de este pequeño ejemplo?

     

  • Cuál es el mejor Framework PHP

    Cuál es el mejor Framework PHP

    Ya escuchaste muchas veces cosas como «¿Cómo que no usás un framework de php?», ¿verdad?

    Y seguramente, de tanto escucharlo te dió curiosidad pero… al buscar te encontrás con que hay tantas opciones para elegir que parece que nunca vas a lograrlo.

    La pregunta es inevitable: ¿cuál es el mejor framework?

    Esto es casi como preguntar ¿a quién querés más? ¿a tu mamá o a tu papá?

    Como en muchas otras áreas de la tecnología la gente tiende a fanatizarse en favor de las herramientas que le resultan más familiares, con lo cual, encontrar opiniones objetivas es ciertamente difícil.

    No voy a dar muchas vueltas, el framework que a mí más me gusta es Symfony, pero de ninguna forma diría que es «el mejor».

    Conozco unos cuantos buenos:

    También hay algunos que se conocen como micro-frameworks:

    Si algo no te convence (el código generado, la facilidad o no para hacer las tareas simples, etc…), es un buen momento para pasar al siguiente candidato por el pre-filtro y así sucesivamente hasta que encuentres la horma de tu zapato 🙂

    ¿Cuál es tu framework favorito?

    En definitiva, la elección siempre será tuya.

    Cómo elegir un framework PHP

    Si bien la última palabra siempre es tuya, te puedo dar algunas ideas que, espero, te ayudarán a tomar la mejor decisión:

    1. ¿Cuándo se hizo el último commit? (Asumo que se trata de un framework de código abierto).
      1. Si ves que hace más de 1 año que no hay un commit lo más probable es que este framework esté discontinuado… mala señal
    2. ¿Qué tan grande es la comunidad de usuarios/desarrolladores?
      1. Si te cuesta mucho encontrar foros de discusión, blogs y demás indicios de que hay mucha gente usando/trabajando en el framework es otro punto en contra
    3. ¿Hay buena documentación?
    4. ¿Tiene alguna finalidad específica? Y en tal caso, ¿coincide con tus necesidades?

    Si las respuestas a estas preguntas te satisfacen el próximo paso es descargar el framework, intentar armar una app sencilla (tipo «Hola Mundo!») y ver cómo te resulta.

    Si algo no te convence (el código generado, la facilidad o no para hacer las tareas simples, etc…), es un buen momento para pasar al siguiente candidato por el pre-filtro y así sucesivamente hasta que encuentres la horma de tu zapato 🙂

  • En qué casos conviene usar un framework PHP

    En qué casos conviene usar un framework PHP

    O en otras palabras: ¿Framework sí o framework no? Qué dilema…

    La respuesta corta es en todos.

    Vamos con la respuesta larga:

    Qué es un Framework

    Existen muchas definiciones diferentes de framework.

    En el consenso general, se trata de un conjunto de librerías que sirven como base de una aplicación.

    Por qué usar un Framework PHP

    Tuve esta discusión bastantes veces con compañeros de trabajo y demás colegas y debo decir que fueron largas horas difícilmente bien invertidas.

    Admito sin emabrgo (nobleza obliga) que fue hace mucho tiempo… es raro escuchar hoy a alguien sostener la postura opuesta (La discusión hoy pasa por cuál framework elegir).

    La respuesta más elaborada (por si tenés tiempo y querés leer un poco más) es esta:

    Salvo que pienses desarrollar un negocio alrededor de un framework de tu propia autoría o tengas mucho tiempo y ganas de aprender mucho PHP, usar un framework estándar es una decisión que no debería tomarte más de 10 segundos.

    ¿Por qué? Simple: ¡tenés muchísimos problemas más importantes por resolver!

    El desarrollo de una aplicación web implica la combinación de una cantidad enorme de elementos (PHP, MySQL, HTML, CSS, JavaScript, etc…) sólo para que «pase algo» y a eso tenés que sumarle lo más importante: la lógica propia de tu aplicación (Para eso te contrataron después de todo ¿no?).

    ¿Para qué te vas a cargar encima con la responsabilidad de resolver miles de problemas que ya están resueltos?

    Por qué NO usar frameworks PHP

    Los frameworks agregan un montón de complejidad que mi aplicación no necesita

    Es probable que esto haya sido cierto hace tiempo. Ls frameworks modernos son sumamente modulares, lo que te permite usar sólo lo que necesitás (Por ejemplo, usando Composer).

    Un framework estándar agrega mucho overhead… mi aplicación necesita ser rápida

    Toda aplicación web necesita ser rápida.

    Cualquier framework decente tiene excelentes herramientas de caché (seguramente mejores que las que podrías implementar vos mismo).

    No confío en el código que no fue producido in-house

    El propio intérprete de PHP no fue producido por tu empresa y, seguramente no tenés mucho problema para justificar su uso, ¿cierto?.

    Diría acá que, salvo que trabajes para Google o Amazon, seguramente el código de un framework bien establecido va a ser mucho mejor que el producido in-house.

    La curva de aprendizaje es muy empinada

    Es posible (siempre dependerá de tu elección de framework), pero se transita una sola vez.

    Cuando hayas dominado el framework, ese conocimiento te va a acompañar en todos los proyectos que encares (incluso si te vas de la empresa donde trabajás actualmente).

    En fin… podría seguir dando razones para usar frameworks, pero me parece que quedó claro el punto.

    Sobre la segunda pregunta (¿Qué framework usar?) escribí esto.