Blog

  • ¿Qué es PHP Composer? Todo lo que Necesitas Saber

    ¿Qué es PHP Composer? Todo lo que Necesitas Saber

    Estás desarrollando una aplicación PHP y te enfrentás a un problema que seguro alguien ya resolvió antes.

    Para qué re-inventar la rueda, ¿cierto?

    Abrís una nueva pestaña, entrás a Google y escribís «Cómo hacer X con php».

    La lista es larga, pero hay un factor común en todos los ejemplos: composer.

    Ahí está otra vez.

    Composer por aquí, composer por allá.

    Por todos lados te muestran cosas como:

    php composer require base/demo-base-code

    Como quien dice «Buenos días».

    Y, a pesar de no ser precisamente un newbie, te frustra tener que copiar y pegar… y rezar.

    Necesitás entender qué es composer de una buena vez.

    Así que… ¿de qué se trata el famoso php composer?

    Qué es Composer

    Composer es un gestor de dependencias para PHP (Similar a lo que npm es para JavaScript o pip para Python).

    Es una aplicación PHP que ayuda a administrar las librerías desarrolladas por terceros que vas a incorporar a tu proyecto.

    De algún modo podrías considerarlo como el heredero de PEAR.

    Hoy día se considera el gestor de dependencias de-facto por unas cuantas buenas razones:

    • Es muy simple de operar
    • Cuenta con un repositorio super completo (Packagist)
    • Disminuye significativamente los problemas de cambio de ambiente de ejecución (Mediante su funcionalidad de congelar dependencias)

    Si nunca lo usaste, te recomiendo que lo pruebes.

    Cómo se instala Composer

    La instalación de composer es sumamente simple. Como dice el sitio getcomposer.org en su página de descargas:

    php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
    php -r "if (hash_file('SHA384', 'composer-setup.php') === '544e09ee996cdf60ece3804abc52599c22b1f40f4323403c44d44fdfdd586475ca9813a858088ffbc1f233e9b180f061') { echo 'Installer verified'; } else { echo 'Installer corrupt'; unlink('composer-setup.php'); } echo PHP_EOL;"
    php composer-setup.php
    php -r "unlink('composer-setup.php');"

    A partir de ahí tendrás disponible un comando llamado composer (Qué sorpresa ¿no?). Si lo ejecutas sin decir más nada te va a dar una lista de los sub-comandos disponibles:

      about           Shows the short information about Composer.
      archive         Creates an archive of this composer package.
      browse          Opens the package's repository URL or homepage in your browser.
      clear-cache     Clears composer's internal package cache.
      clearcache      Clears composer's internal package cache.
      config          Sets config options.
      create-project  Creates new project from a package into given directory.
      depends         Shows which packages cause the given package to be installed.
      diagnose        Diagnoses the system to identify common errors.
      dump-autoload   Dumps the autoloader.
      dumpautoload    Dumps the autoloader.
      exec            Executes a vendored binary/script.
      global          Allows running commands in the global composer dir ($COMPOSER_HOME).
      help            Displays help for a command
      home            Opens the package's repository URL or homepage in your browser.
      info            Shows information about packages.
      init            Creates a basic composer.json file in current directory.
      install         Installs the project dependencies from the composer.lock file if present, or falls back on the composer.json.
      licenses        Shows information about licenses of dependencies.
      list            Lists commands
      outdated        Shows a list of installed packages that have updates available, including their latest version.
      prohibits       Shows which packages prevent the given package from being installed.
      remove          Removes a package from the require or require-dev.
      require         Adds required packages to your composer.json and installs them.
      run-script      Runs the scripts defined in composer.json.
      search          Searches for packages.
      self-update     Updates composer.phar to the latest version.
      selfupdate      Updates composer.phar to the latest version.
      show            Shows information about packages.
      status          Shows a list of locally modified packages.
      suggests        Shows package suggestions.
      update          Updates your dependencies to the latest version according to composer.json, and updates the composer.lock file.
      validate        Validates a composer.json and composer.lock.
      why             Shows which packages cause the given package to be installed.
      why-not         Shows which packages prevent the given package from being installed.

    Te voy a mostrar algunas pocas cosas como para que veas el poder que tiene:

    Cómo funciona Composer

    Composer trabaja con dos archivos de configuración: composer.json y composer.lock.

    El segundo es más bien de uso interno así que no hace falta conocer muchos detalles sobre él.

    El primero en cambio es el que vas a usar bastante a menudo ya que en él se declaran todas las dependencias que tu sistema tendrá.

    Es muy simple de leer (al igual que cualquier json) y permite una gran flexibilidad para definir las dependencias (por ejemplo se puede decir a partir de qué versión se acepta) tanto de librerías escritas en php como del intérprete y sus extensiones.

    Típicamente un archivo composer.json se ve más o menos así:

    {
      "name": "leeway/scrapper",
      "description": "Un pequeño web scrapper",
      "minimum-stability": "stable",
      "license": "proprietary",
      "authors": [
        {
          "name": "Mauro Chojrin",
          "email": "mauro.chojrin@leewayweb.com"
        }
      ],
      "require": {
        "fabpot/goutte": "v3.2.0"
      }
    }

    Donde la parte más importante es lo que está dentro de require (Las dependencias específicas del proyecto).

    Se puede crear el archivo «manualmente» o, mejor, se puede usar el comando composer init que lo hace por nosotros (y aparte permite definir algunas propiedades en forma interactiva).

    Pero claramente, de poco sirve la declaración de las dependencias si no tenemos las librerías instaladas…

    Cómo instalar librerías usando Composer

    Para instalar librerías composer dispone del comando install, el cual se encarga de bajarlas, armar el autoloading y demás… ¡magia!.

    Lo único que te queda por hacer es require_once 'vendor/autoload.php'; donde vayas a usar las librerías instaladas y voilà.

    Hasta acá todo muy lindo, se pueden instalar y usar dependencias con muy poco esfuerzo pero… ¿Te pasó alguna vez que desarrollaste un sistema, en tu computadora andaba 10 puntos pero al subirlo a producción daba algún error raro?

    ¿Te pasó estar horas dando vueltas sin entender nada hasta que te diste cuenta de que la versión 3.4.1 de la librería de envío de mails no era compatible con la 3.4.0 (Instalada en producción)?

    Justamente para eso composer tiene el archivo composer.lock. Si bien es un archivo de texto, ¡no lo toques!.

    Te muestro el que corresponde al composer.json que veías:

    {
        "_readme": [
            "This file locks the dependencies of your project to a known state",
            "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
            "This file is @generated automatically"
        ],
        "content-hash": "cc70bd9c59b4fdbb7e8e632402e15bcc",
        "packages": [
            {
                "name": "fabpot/goutte",
                "version": "v3.2.0",
                "source": {
                    "type": "git",
                    "url": "https://github.com/FriendsOfPHP/Goutte.git",
                    "reference": "8cc89de5e71daf84051859616891d3320d88a9e8"
                },
                "dist": {
                    "type": "zip",
                    "url": "https://api.github.com/repos/FriendsOfPHP/Goutte/zipball/8cc89de5e71daf84051859616891d3320d88a9e8",
                    "reference": "8cc89de5e71daf84051859616891d3320d88a9e8",
                    "shasum": ""
                },
                "require": {
                    "guzzlehttp/guzzle": "^6.0",
                    "php": ">=5.5.0",
                    "symfony/browser-kit": "~2.1|~3.0",
                    "symfony/css-selector": "~2.1|~3.0",
                    "symfony/dom-crawler": "~2.1|~3.0"
                },
                "type": "application",
                "extra": {
                    "branch-alias": {
                        "dev-master": "3.2-dev"
                    }
                },
                "autoload": {
                    "psr-4": {
                        "Goutte\\": "Goutte"
                    }
                },
                "notification-url": "https://packagist.org/downloads/",
                "license": [
                    "MIT"
                ],
                "authors": [
                    {
                        "name": "Fabien Potencier",
                        "email": "fabien@symfony.com"
                    }
                ],
                "description": "A simple PHP Web Scraper",
                "homepage": "https://github.com/FriendsOfPHP/Goutte",
                "keywords": [
                    "scraper"
                ],
                "time": "2016-11-15T16:27:29+00:00"
            },
            {
                "name": "guzzlehttp/guzzle",
                "version": "6.3.0",
                "source": {
                    "type": "git",
                    "url": "https://github.com/guzzle/guzzle.git",
                    "reference": "f4db5a78a5ea468d4831de7f0bf9d9415e348699"
                },
                "dist": {
                    "type": "zip",
                    "url": "https://api.github.com/repos/guzzle/guzzle/zipball/f4db5a78a5ea468d4831de7f0bf9d9415e348699",
                    "reference": "f4db5a78a5ea468d4831de7f0bf9d9415e348699",
                    "shasum": ""
                },
                "require": {
                    "guzzlehttp/promises": "^1.0",
                    "guzzlehttp/psr7": "^1.4",
                    "php": ">=5.5"
                },
                "require-dev": {
                    "ext-curl": "*",
                    "phpunit/phpunit": "^4.0 || ^5.0",
                    "psr/log": "^1.0"
                },
                "suggest": {
                    "psr/log": "Required for using the Log middleware"
                },
                "type": "library",
                "extra": {
                    "branch-alias": {
                        "dev-master": "6.2-dev"
                    }
                },
                "autoload": {
                    "files": [
                        "src/functions_include.php"
                    ],
                    "psr-4": {
                        "GuzzleHttp\\": "src/"
                    }
                },
                "notification-url": "https://packagist.org/downloads/",
                "license": [
                    "MIT"
                ],
                "authors": [
                    {
                        "name": "Michael Dowling",
                        "email": "mtdowling@gmail.com",
                        "homepage": "https://github.com/mtdowling"
                    }
                ],
                "description": "Guzzle is a PHP HTTP client library",
                "homepage": "http://guzzlephp.org/",
                "keywords": [
                    "client",
                    "curl",
                    "framework",
                    "http",
                    "http client",
                    "rest",
                    "web service"
                ],
                "time": "2017-06-22T18:50:49+00:00"
            },
            {
                "name": "guzzlehttp/promises",
                "version": "v1.3.1",
                "source": {
                    "type": "git",
                    "url": "https://github.com/guzzle/promises.git",
                    "reference": "a59da6cf61d80060647ff4d3eb2c03a2bc694646"
                },
                "dist": {
                    "type": "zip",
                    "url": "https://api.github.com/repos/guzzle/promises/zipball/a59da6cf61d80060647ff4d3eb2c03a2bc694646",
                    "reference": "a59da6cf61d80060647ff4d3eb2c03a2bc694646",
                    "shasum": ""
                },
                "require": {
                    "php": ">=5.5.0"
                },
                "require-dev": {
                    "phpunit/phpunit": "^4.0"
                },
                "type": "library",
                "extra": {
                    "branch-alias": {
                        "dev-master": "1.4-dev"
                    }
                },
                "autoload": {
                    "psr-4": {
                        "GuzzleHttp\\Promise\\": "src/"
                    },
                    "files": [
                        "src/functions_include.php"
                    ]
                },
                "notification-url": "https://packagist.org/downloads/",
                "license": [
                    "MIT"
                ],
                "authors": [
                    {
                        "name": "Michael Dowling",
                        "email": "mtdowling@gmail.com",
                        "homepage": "https://github.com/mtdowling"
                    }
                ],
                "description": "Guzzle promises library",
                "keywords": [
                    "promise"
                ],
                "time": "2016-12-20T10:07:11+00:00"
            },
            {
                "name": "guzzlehttp/psr7",
                "version": "1.4.2",
                "source": {
                    "type": "git",
                    "url": "https://github.com/guzzle/psr7.git",
                    "reference": "f5b8a8512e2b58b0071a7280e39f14f72e05d87c"
                },
                "dist": {
                    "type": "zip",
                    "url": "https://api.github.com/repos/guzzle/psr7/zipball/f5b8a8512e2b58b0071a7280e39f14f72e05d87c",
                    "reference": "f5b8a8512e2b58b0071a7280e39f14f72e05d87c",
                    "shasum": ""
                },
                "require": {
                    "php": ">=5.4.0",
                    "psr/http-message": "~1.0"
                },
                "provide": {
                    "psr/http-message-implementation": "1.0"
                },
                "require-dev": {
                    "phpunit/phpunit": "~4.0"
                },
                "type": "library",
                "extra": {
                    "branch-alias": {
                        "dev-master": "1.4-dev"
                    }
                },
                "autoload": {
                    "psr-4": {
                        "GuzzleHttp\\Psr7\\": "src/"
                    },
                    "files": [
                        "src/functions_include.php"
                    ]
                },
                "notification-url": "https://packagist.org/downloads/",
                "license": [
                    "MIT"
                ],
                "authors": [
                    {
                        "name": "Michael Dowling",
                        "email": "mtdowling@gmail.com",
                        "homepage": "https://github.com/mtdowling"
                    },
                    {
                        "name": "Tobias Schultze",
                        "homepage": "https://github.com/Tobion"
                    }
                ],
                "description": "PSR-7 message implementation that also provides common utility methods",
                "keywords": [
                    "http",
                    "message",
                    "request",
                    "response",
                    "stream",
                    "uri",
                    "url"
                ],
                "time": "2017-03-20T17:10:46+00:00"
            },
            {
                "name": "psr/http-message",
                "version": "1.0.1",
                "source": {
                    "type": "git",
                    "url": "https://github.com/php-fig/http-message.git",
                    "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363"
                },
                "dist": {
                    "type": "zip",
                    "url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363",
                    "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363",
                    "shasum": ""
                },
                "require": {
                    "php": ">=5.3.0"
                },
                "type": "library",
                "extra": {
                    "branch-alias": {
                        "dev-master": "1.0.x-dev"
                    }
                },
                "autoload": {
                    "psr-4": {
                        "Psr\\Http\\Message\\": "src/"
                    }
                },
                "notification-url": "https://packagist.org/downloads/",
                "license": [
                    "MIT"
                ],
                "authors": [
                    {
                        "name": "PHP-FIG",
                        "homepage": "http://www.php-fig.org/"
                    }
                ],
                "description": "Common interface for HTTP messages",
                "homepage": "https://github.com/php-fig/http-message",
                "keywords": [
                    "http",
                    "http-message",
                    "psr",
                    "psr-7",
                    "request",
                    "response"
                ],
                "time": "2016-08-06T14:39:51+00:00"
            },
            {
                "name": "symfony/browser-kit",
                "version": "v3.3.10",
                "source": {
                    "type": "git",
                    "url": "https://github.com/symfony/browser-kit.git",
                    "reference": "317d5bdf0127f06db7ea294186132b4f5b036839"
                },
                "dist": {
                    "type": "zip",
                    "url": "https://api.github.com/repos/symfony/browser-kit/zipball/317d5bdf0127f06db7ea294186132b4f5b036839",
                    "reference": "317d5bdf0127f06db7ea294186132b4f5b036839",
                    "shasum": ""
                },
                "require": {
                    "php": "^5.5.9|>=7.0.8",
                    "symfony/dom-crawler": "~2.8|~3.0"
                },
                "require-dev": {
                    "symfony/css-selector": "~2.8|~3.0",
                    "symfony/process": "~2.8|~3.0"
                },
                "suggest": {
                    "symfony/process": ""
                },
                "type": "library",
                "extra": {
                    "branch-alias": {
                        "dev-master": "3.3-dev"
                    }
                },
                "autoload": {
                    "psr-4": {
                        "Symfony\\Component\\BrowserKit\\": ""
                    },
                    "exclude-from-classmap": [
                        "/Tests/"
                    ]
                },
                "notification-url": "https://packagist.org/downloads/",
                "license": [
                    "MIT"
                ],
                "authors": [
                    {
                        "name": "Fabien Potencier",
                        "email": "fabien@symfony.com"
                    },
                    {
                        "name": "Symfony Community",
                        "homepage": "https://symfony.com/contributors"
                    }
                ],
                "description": "Symfony BrowserKit Component",
                "homepage": "https://symfony.com",
                "time": "2017-10-02T06:42:24+00:00"
            },
            {
                "name": "symfony/css-selector",
                "version": "v3.3.10",
                "source": {
                    "type": "git",
                    "url": "https://github.com/symfony/css-selector.git",
                    "reference": "07447650225ca9223bd5c97180fe7c8267f7d332"
                },
                "dist": {
                    "type": "zip",
                    "url": "https://api.github.com/repos/symfony/css-selector/zipball/07447650225ca9223bd5c97180fe7c8267f7d332",
                    "reference": "07447650225ca9223bd5c97180fe7c8267f7d332",
                    "shasum": ""
                },
                "require": {
                    "php": "^5.5.9|>=7.0.8"
                },
                "type": "library",
                "extra": {
                    "branch-alias": {
                        "dev-master": "3.3-dev"
                    }
                },
                "autoload": {
                    "psr-4": {
                        "Symfony\\Component\\CssSelector\\": ""
                    },
                    "exclude-from-classmap": [
                        "/Tests/"
                    ]
                },
                "notification-url": "https://packagist.org/downloads/",
                "license": [
                    "MIT"
                ],
                "authors": [
                    {
                        "name": "Jean-François Simon",
                        "email": "jeanfrancois.simon@sensiolabs.com"
                    },
                    {
                        "name": "Fabien Potencier",
                        "email": "fabien@symfony.com"
                    },
                    {
                        "name": "Symfony Community",
                        "homepage": "https://symfony.com/contributors"
                    }
                ],
                "description": "Symfony CssSelector Component",
                "homepage": "https://symfony.com",
                "time": "2017-10-02T06:42:24+00:00"
            },
            {
                "name": "symfony/dom-crawler",
                "version": "v3.3.10",
                "source": {
                    "type": "git",
                    "url": "https://github.com/symfony/dom-crawler.git",
                    "reference": "40dafd42d5dad7fe5ad4e958413d92a207522ac1"
                },
                "dist": {
                    "type": "zip",
                    "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/40dafd42d5dad7fe5ad4e958413d92a207522ac1",
                    "reference": "40dafd42d5dad7fe5ad4e958413d92a207522ac1",
                    "shasum": ""
                },
                "require": {
                    "php": "^5.5.9|>=7.0.8",
                    "symfony/polyfill-mbstring": "~1.0"
                },
                "require-dev": {
                    "symfony/css-selector": "~2.8|~3.0"
                },
                "suggest": {
                    "symfony/css-selector": ""
                },
                "type": "library",
                "extra": {
                    "branch-alias": {
                        "dev-master": "3.3-dev"
                    }
                },
                "autoload": {
                    "psr-4": {
                        "Symfony\\Component\\DomCrawler\\": ""
                    },
                    "exclude-from-classmap": [
                        "/Tests/"
                    ]
                },
                "notification-url": "https://packagist.org/downloads/",
                "license": [
                    "MIT"
                ],
                "authors": [
                    {
                        "name": "Fabien Potencier",
                        "email": "fabien@symfony.com"
                    },
                    {
                        "name": "Symfony Community",
                        "homepage": "https://symfony.com/contributors"
                    }
                ],
                "description": "Symfony DomCrawler Component",
                "homepage": "https://symfony.com",
                "time": "2017-10-02T06:42:24+00:00"
            },
            {
                "name": "symfony/polyfill-mbstring",
                "version": "v1.6.0",
                "source": {
                    "type": "git",
                    "url": "https://github.com/symfony/polyfill-mbstring.git",
                    "reference": "2ec8b39c38cb16674bbf3fea2b6ce5bf117e1296"
                },
                "dist": {
                    "type": "zip",
                    "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/2ec8b39c38cb16674bbf3fea2b6ce5bf117e1296",
                    "reference": "2ec8b39c38cb16674bbf3fea2b6ce5bf117e1296",
                    "shasum": ""
                },
                "require": {
                    "php": ">=5.3.3"
                },
                "suggest": {
                    "ext-mbstring": "For best performance"
                },
                "type": "library",
                "extra": {
                    "branch-alias": {
                        "dev-master": "1.6-dev"
                    }
                },
                "autoload": {
                    "psr-4": {
                        "Symfony\\Polyfill\\Mbstring\\": ""
                    },
                    "files": [
                        "bootstrap.php"
                    ]
                },
                "notification-url": "https://packagist.org/downloads/",
                "license": [
                    "MIT"
                ],
                "authors": [
                    {
                        "name": "Nicolas Grekas",
                        "email": "p@tchwork.com"
                    },
                    {
                        "name": "Symfony Community",
                        "homepage": "https://symfony.com/contributors"
                    }
                ],
                "description": "Symfony polyfill for the Mbstring extension",
                "homepage": "https://symfony.com",
                "keywords": [
                    "compatibility",
                    "mbstring",
                    "polyfill",
                    "portable",
                    "shim"
                ],
                "time": "2017-10-11T12:05:26+00:00"
            }
        ],
        "packages-dev": [],
        "aliases": [],
        "minimum-stability": "stable",
        "stability-flags": [],
        "prefer-stable": false,
        "prefer-lowest": false,
        "platform": [],
        "platform-dev": []
    }

    Es un json por dentro (Bastante más extenso y complejo que el original) pero su objetivo es el de congelar las dependencias que efectivamente se están usando en tu código.

    Si mirás de nuevo el composer.json vas a notar que la versión de Goutte está expresada en forma explícita (v3.2.0) pero, si mirás un poco más profundamente (por ejemplo en el archivo vendor/fabpot/goutte/composer.json) verás que hay otras que no son tan estrictas:

    {
        "name": "fabpot/goutte",
        "type": "application",
        "description": "A simple PHP Web Scraper",
        "keywords": ["scraper"],
        "homepage": "https://github.com/FriendsOfPHP/Goutte",
        "license": "MIT",
        "authors": [
            {
                "name": "Fabien Potencier",
                "email": "fabien@symfony.com"
            }
        ],
        "require": {
            "php": ">=5.5.0",
            "symfony/browser-kit": "~2.1|~3.0",
            "symfony/css-selector": "~2.1|~3.0",
            "symfony/dom-crawler": "~2.1|~3.0",
            "guzzlehttp/guzzle": "^6.0"
        },
        "autoload": {
            "psr-4": { "Goutte\\": "Goutte" }
        },
        "extra": {
            "branch-alias": {
                "dev-master": "3.2-dev"
            }
        }
    }

    Aquí dice que se necesita una versión de Guzzle al menos 6.0.

    Si ves en composer.lock notarás que la instalación de Guzzle es clara:

    ...
            {
                "name": "guzzlehttp/guzzle",
                "version": "6.3.0",
    ...
            }

    Y es que 6.3.0 es al menos 6.0 pero, al momento de distribuir el código necesitamos saber exactamente qué versión estamos usando.

    De modo que el composer.lock se genera:

    1. Recorriendo el árbol de dependencias completo
    2. Especificando las versiones concretas que están instaladas actualmente

    De esta forma, si el código se lleva a un entorno nuevo (o hay que reinstalarlo por alguna razón), la información en composer.lock asegura que se esté usando exactamente el mismo set de dependencias que se usó en testing (Si el testing no fue suficiente es otro tema…).

    Actualización de dependencias

    Por otro lado, si querés probar lo último disponible podés (¡en desarrollo!) ejecutar el comando composer update y composer se encargará nuevamente de descargar las nuevas versiones de tus dependencias (siempre respetando tus especificaciones) para que puedas probarlas y generar un nuevo composer.lock (Que obviamente vas a tener que committear).

    Remoción de dependencias

    Esto puede ser algo más tricky (Siempre es más fácil meter cosas que no se usarán que sacarlas y arriesgarse pero… si sabés lo que hacés…). El comando que se usa para esto es composer remove.

    ¡A usarlo con juicio!

    Y si necesitas ayuda…

    Siempre tenés la posibilidad de ejecutar el comando composer help <comando>.

    Conclusión

    Ahora ya sabés qué es Composer y cómo te puede simplificar la vida.

    La próxima vez que vayas a leer un ejemplo de código comprenderás los pasos de la instalación y podrás leer el archivo composer.json para ver qué otras dependencias vienen incluidas.

    ¿Todavía no descargaste composer?

    ¡Hacelo ahora mismo!

    Acá te dejo el link otra vez, ya vas a tener tiempo de volver acá a agradecerme.

  • 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.

  • Por qué usar una máquina virtual para proyectos PHP

    Por qué usar una máquina virtual para proyectos PHP

    Respuesta rápida: para evitar sorpresas desagradables.

    Paso a detallar un poco de qué estoy hablando con un par de historias que me pasaron hace unos cuantos años durante el desarrollo de un proyecto personal.

    Como la mayoría de los desarrolladores que están empezando, tenía instalado en mi computadora (Windows en aquel entonces) el paquete XAMPP y mi forma de trabajo era programar y probar en mi propia computadora y luego, mediante FTP, subir mis cambios al hosting que había contratado con la empresa de un amigo.

    Un día una de las últimas actualizaciones que había realizado a mi código funcionaba perfectamente en mi casa, pero al subirla a mi hosting las cosas eran un poco diferentes (léase: no andaba nada :p).

    Hago la historia corta: yo había escrito los nombres de las tablas de mis consultas en mayúsculas (algo como SELECT * FROM USERS), cuando los nombres en la base estaban en minúsculas. Claro, mientras estaba en un entorno incapaz de distinguir mayúsculas de minúsculas (Windows) no había problema, pero al deployar sobre un BSD que sí lo era… aparecieron los problemas.

    Tardé unos cuantos días en darme cuenta del problema (Y unas cuantas veces pensé en cambiar de profesión).

    Otro problema similar que tuve fue todavía más difícil de encontrar: mi versión de MySQL era un poquitín superior a la que estaba usando el hosting (Con lo cual, una función de la que mi código dependía simplemente no estaba disponible en Producción).

    Conclusión:

    El modo más seguro de evitar que aparezcan sorpresas al llevar tu código a Producción (O a cualquier otro ambiente para el caso), es programar en un entorno lo más parecido posible al productivo.

    Hay dos opciones básicas para lograr esto:

    1. Tener tu propio servidor que puedas configurar de acuerdo a las necesidades de tu aplicación (Probablemente un VPS)
    2. Configurar tu entorno de desarrollo de acuerdo a las capacidades que te brindará el servidor donde vayas a montar tu aplicación

    Independientemente de cuál sea tu elección, tener una máquina virtual específica para tu proyecto es la forma más práctica de generar código que corra sin problemas en el ambiente productivo.

    Así que en definitiva, la pregunta sería «¿Por qué NO usar una máquina virtual para proyectos PHP?»

  • Una máquina virtual lista para PHP+Symfony2+XDebug

    Charlando con algunos amigos desarrolladores php surgió un tema que les estaba resultando complicado, así que decidí poner mi pequeño granito de arena (para ellos y para otros que tal vez estén pasando por lo mismo).

    Ya habíamos hablado del por qué usar una máquina virtual para proyectos PHP. Ya estaba claro que usar un framework es más conveniente que no usarlo (independientemente de cuál fuera) y les había comentado sobre mis herramientas favoritas de automatización (Ansible y Vagrant).

    Todos estábamos de acuerdo «en la teoría», pero a la hora de pasar a la práctica se veían algo frustrados por no poder lograr tener una máquina virtual que fuese fácil de usar y que soportara, entre otras cosas, el uso de XDebug.

    Lo que te voy a mostrar a continuación son los archivos de configuración que yo usé en el último proyecto que hice en php:

    El archivo playbook.yml:

    ---
    - hosts: all
     sudo: true
     tasks:
    
    - name: create /var/www
     file: path=/var/www state=directory
    
    - name: create site symlink
     file: src=/vagrant dest=/var/www/site state=link
     notify: restart apache
    
    - name: install misc packages
     apt: name={{ item }} state=latest update_cache=true
     with_items:
     - ruby2.0
     - ruby2.0-dev
     - git
     - curl
     - unzip
     - vim
    
    - name: Symlink exists for Ruby 2.0
     file: src=/usr/bin/ruby2.0 dest=/usr/local/bin/ruby state=link
    
    - name: Symlink exists for Ruby Gems 2.0
     file: src=/usr/bin/gem2.0 dest=/usr/local/bin/gem state=link
    
    - name: install language packs for locale support
     apt: name={{ item }} state=latest
     with_items:
     - language-pack-de-base
     - language-pack-es-base
    
    # Apache2
    
    - name: ensure apache is installed
     apt: name=apache2 state=present
    
    - name: make sure apache is running
     action: service name=apache2 state=started enabled=true
    
    - file: src=/etc/apache2/mods-available/rewrite.load dest=/etc/apache2/mods-enabled/rewrite.load state=link
     notify: restart apache
    
    - file: src=/etc/apache2/mods-available/headers.load dest=/etc/apache2/mods-enabled/headers.load state=link
     notify: restart apache
    
    - copy: src=/vagrant/ansible/templates/site.conf dest=/etc/apache2/sites-available/site.conf remote_src=true
     notify: restart apache
    
    - file: src=/etc/apache2/sites-available/site.conf dest=/etc/apache2/sites-enabled/site.conf state=link
     notify: restart apache
    
    - file: path=/etc/apache2/sites-enabled/000-default.conf state=absent
     notify: restart apache
    
    - file: path=/etc/apache2/conf.d state=directory
    
    - copy: src=/vagrant/ansible/templates/fqdn.conf dest=/etc/apache2/conf.d/fqdn.conf remote_src=true
     notify: restart apache
    
    - copy: src=/vagrant/ansible/templates/nosendfile.conf dest=/etc/apache2/conf.d/nosendfile.conf remote_src=true
     notify: restart apache
    
    # MySQL
    
    - name: install MySQL
     apt: name={{ item }} state=latest
     with_items:
     - mysql-server
     - mysql-client
     - python-mysqldb
    
    - name: add mysql user
     mysql_user: name=vagrant
     host={{ item }}
     password=vagrant priv=*.*:ALL,GRANT
     login_user=root
     login_password=
     with_items:
     - '%'
     - localhost
    
    - name: create mysql databases
     mysql_db: name={{ item }}
     state=present
     with_items:
     - site_development
     - site_development_stats
     - site_testing
     - site_testing_stats
    
    - file: path=/etc/mysql/conf.d state=directory
     - name: Set MySQL number of connections
     copy: src=/vagrant/ansible/templates/max_connections.cnf dest=/etc/mysql/conf.d/max_connections.cnf remote_src=true
     notify: restart mysql
    
    - name: Install mysql command line client configuration file
     copy: src=/vagrant/ansible/templates/my.cnf dest=/home/vagrant/.my.cnf owner=vagrant group=vagrant remote_src=true
    
    # PHP
    
    - name: add php5.6 ppa
     apt_repository: repo='ppa:ondrej/php'
    
    - name: install PHP5.6 packages
     apt: name={{ item }} state=latest
     with_items:
     - php5.6
     - libapache2-mod-php5.6
     - php5.6-cli
     - php5.6-dev
     - php5.6-mysql
     - php-pear
     - php5.6-mcrypt
     - php5.6-gd
     - php5.6-curl
     - php5.6-xdebug
     - php5.6-memcache
     - php5.6-memcached
     - php5.6-readline
     - php5.6-xml
     - php5.6-mbstring
     - php5.6-zip
    
    - file: path=/etc/php5.6/conf.d state=directory
     - file: path=/etc/php5.6/cli/conf.d state=directory
     - file: path=/etc/php5.6/apache2/conf.d state=directory
    
    - copy: src=/vagrant/ansible/templates/php-site.ini dest=/etc/php5.6/conf.d/php-site.ini remote_src=true
     notify: restart apache
    
    - name: configure xdebug
     copy: src=templates/xdebug.ini dest=/etc/php/5.6/mods-available/xdebug.ini
     notify: restart apache
    
    - name: symlink common php configuration for cli handler
     file: src=/etc/php5.6/conf.d/php-site.ini dest=/etc/php5.6/cli/conf.d/php-site.ini state=link
     notify: restart apache
    
    - name: symlink common php configuration for apache2 handler
     file: src=/etc/php5.6/conf.d/php-site.ini dest=/etc/php5.6/apache2/conf.d/php-site.ini state=link
     notify: restart apache
    
    # phpmyadmin
    
    - name: install phpmyadmin
     apt: name=phpmyadmin state=latest
    
    # Assets compilation
    
    - name: add nodejs ppa
     apt_repository: repo='ppa:chris-lea/node.js'
    
    - name: install nodejs
     apt: name=nodejs state=latest
    
    # Set up site
    
    - file: src=/vagrant dest=/var/www/site state=link
     - file: path={{ item }} owner=vagrant group=vagrant mode=0777 state=directory
     with_items:
     - /var/cache/site
     - /var/cache/site/cache
     - /var/cache/site/clockwork
     - /var/cache/site/logs
     - /var/cache/site/meta
     - /var/cache/site/sessions
     - /var/cache/site/views
    
    - name: ensure once more that 000-default.conf is deleted
     file: path=/etc/apache2/sites-enabled/000-default.conf state=absent
     notify: restart apache
    
    - name: ensure that phpmyadmin's stock config is deleted
     file: path=/etc/apache2/conf.d/phpmyadmin.conf state=absent
    
    - name: set proper permissions for app/reports directory
     file: path=/vagrant/app/reports group=www-data owner=vagrant mode=0775 state=directory
    
    # Common stuff
    
    - name: Install Composer
     shell: curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer creates=/usr/local/bin/composer
    
    handlers:
     - name: restart apache
     action: service name=apache2 state=restarted
     - name: restart mysql
     action: service name=mysql state=restarted
     - name: restart beanstalkd
     action: service name=beanstalkd state=restarted

    Los archivos templates (deben estar en el directorio ansible/templates dentro de tu proyecto):

    site.conf:

    <VirtualHost *:80>
     ServerName myApp
     DocumentRoot /var/www/site/web
    
    <Directory />
     Options FollowSymLinks
     AllowOverride None
     </Directory>
    
    <Directory /var/www/site/web>
     DirectoryIndex app_dev.php
     Options Indexes FollowSymLinks MultiViews
     AllowOverride All
     Order allow,deny
     allow from all
     </Directory>
    
    ErrorLog /var/log/apache2/error.log
     LogLevel warn
     CustomLog /var/log/apache2/access.log combined
    
    ## enable phpmyadmin
    
    Alias /phpmyadmin /usr/share/phpmyadmin
    
    <Directory /usr/share/phpmyadmin>
     Options FollowSymLinks
     DirectoryIndex index.php
    
    <IfModule mod_php5.c>
     AddType application/x-httpd-php .php
    
    php_flag magic_quotes_gpc Off
     php_flag track_vars On
     php_flag register_globals Off
     php_admin_flag allow_url_fopen Off
     php_value include_path .
     php_admin_value upload_tmp_dir /var/lib/phpmyadmin/tmp
     php_admin_value open_basedir /usr/share/phpmyadmin/:/etc/phpmyadmin/:/var/lib/phpmyadmin/
     </IfModule>
    
    </Directory>
    </VirtualHost>

    fqdn.conf

    ServerName Localhost

    nosendfile.conf

    EnableSendfile off

    max_connections.cnf

    [mysqld]
    max_connections = 400

    my.cnf

    [client]
    user=vagrant
    password=vagrant

    php-site.ini

    max_execution_time = 30000
    memory_limit = 512M
    display_errors = On
    disable_functions =

    xdebug.ini

    zend_extension=xdebug.so
    xdebug.remote_enable=On
    xdebug.remote_connect_back=On

    Para iniciar el proyecto, basta con copiar y pegar esto en un archivo llamado Vagrantfile (en la raíz de tu proyecto):

    # -*- mode: ruby -*-
     # vi: set ft=ruby :
    
    # All Vagrant configuration is done below. The "2" in Vagrant.configure
     # configures the configuration version (we support older styles for
     # backwards compatibility). Please don't change it unless you know what
     # you're doing.
     Vagrant.configure("2") do |config|
     # The most common configuration options are documented and commented below.
     # For a complete reference, please see the online documentation at
     # https://docs.vagrantup.com.
    
    # Every Vagrant development environment requires a box. You can search for
     # boxes at https://atlas.hashicorp.com/search.
     config.vm.box = "ubuntu/trusty64"
    
    # Disable automatic box update checking. If you disable this, then
     # boxes will only be checked for updates when the user runs
     # `vagrant box outdated`. This is not recommended.
     # config.vm.box_check_update = false
    
    # Create a forwarded port mapping which allows access to a specific port
     # within the machine from a port on the host machine. In the example below,
     # accessing "localhost:8080" will access port 80 on the guest machine.
     config.vm.network "forwarded_port", guest: 80, host: 8080
    
    # Create a private network, which allows host-only access to the machine
     # using a specific IP.
     config.vm.network "private_network", ip: "192.168.33.10"
    
    # Create a public network, which generally matched to bridged network.
     # Bridged networks make the machine appear as another physical device on
     # your network.
     # config.vm.network "public_network"
    
    # Share an additional folder to the guest VM. The first argument is
     # the path on the host to the actual folder. The second argument is
     # the path on the guest to mount the folder. And the optional third
     # argument is a set of non-required options.
     # config.vm.synced_folder "../data", "/vagrant_data"
    
    # Provider-specific configuration so you can fine-tune various
    
    # backing providers for Vagrant. These expose provider-specific options.
     # Example for VirtualBox:
     #
     config.vm.provider "virtualbox" do |vb|
     # # Display the VirtualBox GUI when booting the machine
     # vb.gui = true
     #
     # # Customize the amount of memory on the VM:
     vb.memory = "2048"
     end
     #
     # View the documentation for the provider you are using for more
     # information on available options.
    
    # Define a Vagrant Push strategy for pushing to Atlas. Other push strategies
     # such as FTP and Heroku are also available. See the documentation at
     # https://docs.vagrantup.com/v2/push/atlas.html for more information.
     # config.push.define "atlas" do |push|
     # push.app = "YOUR_ATLAS_USERNAME/YOUR_APPLICATION_NAME"
     # end
    
    # Enable provisioning with a shell script. Additional provisioners such as
     # Puppet, Chef, Ansible, Salt, and Docker are also available. Please see the
     # documentation for more information about their specific syntax and use.
     # config.vm.provision "shell", inline: <<-SHELL
     # apt-get update
     # apt-get install -y apache2
     # SHELL
    
    #
     # Run Ansible from the Vagrant Host
     #
     config.vm.provision "ansible" do |ansible|
       ansible.playbook = "ansible/playbook.yml"
       ansible.verbose = "vv"
     end
    
    # Symfony needs to be able to write to it's cache, logs and sessions directory in var/
     config.vm.synced_folder "./var", "/vagrant/var",
       :owner => 'vagrant',
       :group => 'www-data',
       :mount_options => ["dmode=777","fmode=777"]
     end

    y ejecutar el comando (Obviamente, habiendo instalado Vagrant previamente…):

     vagrant up

    Y comienza la magia 🙂

    Un ratito después (Dependiendo de la conexión que tengas) vas a tener una máquina lista para desarrollar tu aplicación basada en Symfony2.

    Y ahora… ¡a codear!

    ¿Me olvidé de algo? ¡Avisame en los comentarios!

  • Cómo obtener la cotización del día de una acción con PHP

    Todo lo que voy a mostrarte acá se basa en la API de Yahoo Finance.

    Lo primero que tenés que hacer es instalar composer.

    Segundo, inicializar el proyecto:

    php composer.phar init

    Tercero: agregar la dependencia del paquete https://github.com/scheb/yahoo-finance-api:

    php composer.phar require scheb/yahoo-finance-api

    Y después podés usar un código como este:

    #!/usr/bin/php
    <?php
    
    require __DIR__ . '/vendor/autoload.php';
    
    $client = new \Scheb\YahooFinanceApi\ApiClient();
    
    $d = new DateTime($argv[2]);
    
    echo "Buscando ".$argv[1]." en fecha: ".$d->format('d/m/y').PHP_EOL;
    try {
      $data = $client->getHistoricalData($argv[1], $d, $d);
    
      echo $data['query']['results']['quote']['Close'].PHP_EOL;
    } catch ( Exception $e ) {
      echo $e->getMessage().PHP_EOL;
    }
    

    En este ejemplo, lo que tenés es una utilidad de línea de comandos que recibe dos parámetros:

    1. El ticker (Símbolo del papel en cuestión, por ejemplo TS para la acción de Tenaris)
    2. La fecha.

    La llamada sería así:

    php get_stock_price.php TS Yesterday

    y el resultado será algo como:

    Buscando TS en fecha: 15/12/16
    34.220001
    
  • Webscrapping con PHP

    Webscrapping con PHP

    Me encontré recientemente con este problema desarrollando un sistema para un cliente y creo (¡y espero!) que mi experiencia pueda ayudar a otros.

    El desafío era el siguiente: nuestro cliente es una empresa que se dedica a la administración de activos financieros. Como parte de su operatoria, requieren la consolidación de información que actualmente está dispersa en una serie de planillas Excel.

    Parte de esa información se refiere a movimientos de acciones y bonos. Una de las tareas que se realizaba manualmente era el cálculo de cuánto dinero se había movido al realizar una compra o venta de alguno de estos instrumentos (Simple: cantidad de títulos por precio del título al día de la transacción).

    La complejidad de este cálculo reside en cómo conseguir el precio que tenía el activo al día en que se realizó la transacción. Antes de la intervención de Leeway esta tarea estaba a cargo de una empleada de la compañía (Entre muchas otras, tenía que buscar en el sitio de Yahoo Finance u otro similar y completar ese dato).

    Desarrollamos una aplicación que fuera capaz de consultar esa información y realizar ese cálculo en forma automatizada.

    No es realmente complicado hacerlo cuando se cuenta con una API bien hecha y documentada (Incluso mejor si tenemos a mano un SDK para PHP).

    Lo complicado del tema fue sacar la información de bonos, para la cual no encontramos ninguna fuente pública que tuviera buena información (¿Conocés alguna?, te invito a que dejes un comentario 🙂 ), con lo cual… no quedó opción más que arremangarse y hacer algo de web scrapping (Todo sea por ahorrarle unas horas de rastreo todos los meses a un cliente).

    Así que ahí nos metimos, cURL y SimpleHTMLDom en mano, a remover la maleza y a ver qué encontrábamos.

    Y la verdad… lo que encontramos no fue nada bonito. La primera misión fue entender todo el camino que un usuario humano tenía que recorrer para llegar a la información que nosotros queríamos obtener.

    Una vez que tuvimos esa información, nuestro primer intento fue apuntarle a la última URL con un simple GET y escarbar el resultado… no señor, ¿tan fácil iba a ser el tema?

    Lo primero que notamos era que la URL final tenía poco en común con la inicial (y sin acceso a la base de datos que andaba por detrás… difícil hacer la conversión…). Bien, retrocedamos dos casilleros.

    Nuevamente, bajemos el contenido de la primera URL y busquemos el link que necesitamos (¡Gracias S.C. Chen y compañía por ponerle sintaxis tipo jQuery al SimpleHTMLDom!).

    ¡Primera prueba superada! Tenemos una nueva página que está un pasito más cerca. Veámosla un poco más de cerca… ¡ahá! ¡Se trata de un formulario que va por POST! Ningún problema, nuestro amigo FireBug nos mostrará el camino. Ah, pero esto es muy simple… una pequeña llamada Ajax y voilà.

    Otra vez… ¡no tan rápido! Faltan los parámetros invisibles que se generan del lado del servidor… jejeje (Léase con risa de programador malicioso). Ok, volvamos a revisar ese HTML. Bien, acá están esos input hidden, no hay problema, los agregamos y listo.

    ¿Cómo que 404? ¡Si estoy viendo la información! ¿qué te pasa cURL?

    ¿Cómo? ¿Que el valor de un campo no es un literal si no una expresión?… qué ganas de complicarle la vida al prójimo… bueno… usemos el eval de php. Esta parte sí te la puedo mostrar:

    foreach ( $dom->find('.ajax-token') as $token ) {
      if ( $token->attr['name'] == '__atcrv' ) {
        $atcrv = eval('return '.$token->attr['value'].';');
      }
    }<div class="open_grepper_editor" title="Edit &amp; Save To Grepper">

    Y ahora, ¿qué otra sorpresita hay por ahí? ¿La respuesta (si da 200) viene gzipeada? 0 problema: gzdecode se encarga y por fín, tenemos a mano la tablita con los precios del bono buscado para el día buscado. Muchas gracias, buenas noches.


    La historia fue para poner en contexto, pasando en limpio (para generalizar un poco también):

    1. Todo lo que un navegador hace, cURL puede hacerlo también (Iba a decir, todo lo que una persona hace, pero después me vino a la mente el reCaptcha).
    2. Este mecanismo no es ni de lejos ideal. Basta con un pequeño cambio en el maquetado del sitio para que todo se rompa (Pero bueno… si no disponemos de una buena API, no creo que haya otro mucho mejor)
    3. No hay que temerle a un poco de ingeniería inversa 🙂
    4. La solapa Net de FireBug da un montón de información súper útil.

    ¿Me olvidé de algo importante?