tag:oliverservin.com,2005:/feed <![CDATA[Desarrollador Laravel Freelance | Oliver Servín]]> 2023-03-21T16:38:40+00:00 tag:oliverservin.com,2005:World::Post/vscode-cli 2023-03-21T00:00:00+00:00 2023-03-21T00:00:00+00:00 <![CDATA[Cómo utilizar la interfaz de línea de comandos de VS Code]]>

Al instalar VS Code, también te dará acceso al comando code desde el terminal.

code

Aunque si estás en Mac OS, tendrás que ir a la paleta de comandos y encontrar la opción para instalar el comando code en tu path de usuario.

Puedes validar la instalación del comando ejecutando code --v para ver la versión actual de VS Code.

Si quieres ver más información, puedes ejecutar code --status para ver el uso de procesamiento e información de diagnóstico adicional.

Pero la parte más útil de la CLI es simplemente abrir archivos y carpetas con VS Code.

Si estás en la terminal y quieres abrir el directorio de trabajo actual, simplemente escribe code ., y se abrirá el directorio en VS Code. O en lugar de un punto, puedes escribir la ruta relativa al directorio que deseas abrir.

code ./laravel-new

Fantástico, pero una cosa que encuentro especialmente útil es usar este comando cuando se abren archivos de configuración en un proyecto existente.

Por ejemplo, podrías querer editar un archivo de configuración global como el perfil de bash. En lugar de abrir una nueva ventana, puedo simplemente escribir código con la ruta a ese archivo y abrirlo aquí directamente en este espacio de trabajo.

code ~/.bash_profile

Eso es mucho más fácil que abrir una nueva ventana o utilizar algo como Vim si no estás familiarizado con él.

Pero la conclusión es emplear la CLI de code para poder acceder más rápido a un directorio o archivo y abrirlo en VS Code.

]]>
tag:oliverservin.com,2005:World::Post/instalar-laravel 2023-03-16T00:00:00+00:00 2023-03-16T00:00:00+00:00 <![CDATA[Cómo instalar y comenzar a utilizar Laravel rápidamente]]>

Para comenzar a utilizar Laravel, necesitamos descargarlo e instalarlo. Pero, antes de eso, hay algunos pasos de configuración que debemos realizar.

Configuración necesaria antes de comenzar

Necesitarás que PHP sea ejecutable de forma local. Puedes consultar la documentación de PHP para obtener instrucciones específicas de cómo instalar PHP según tu plataforma.

Uso del instalador de Laravel

Para crear un nuevo proyecto vamos a usar el instalador global de Laravel. Necesitaremos descargar Composer. Para comprobar que Composer esté instalado, simplemente ejecuta el comando composer.

Una vez que tengas PHP instalado y funcionando localmente, y Composer instalado, estás listo para comenzar.

Para instalar el instalador de Laravel, ejecuta el comando require globalmente de Composer.

composer global require laravel/installer

Tener el instalador, nos permite iniciar rápidamente aplicaciones Laravel.

Iniciar un nuevo proyecto con Laravel

Para empezar un nuevo proyecto, usa el comando laravel new seguido del nombre del proyecto. Por ejemplo, laravel-basico.

laravel new laravel-basico

Laravel comenzará a descargar todas sus dependencias. También instalará toda su estructura básica.

Ejecución de Laravel con un servidor PHP local

Para empezar a utilizar Laravel, la forma más sencilla es ejecutarlo con un servidor PHP local. Aunque existen otros métodos, son más adecuados para usuarios experimentados.

Cuando ejecutamos artisan, tenemos acceso a una variedad de comandos que hacen que desarrollar en Laravel sea más simple.

La mayoría de los comandos listados son comandos de tipo make.

Vamos a emplear el comando serve de Artisan para lanzar la aplicación en localhost empleando el puerto 8000.

php artisan serve

Después, vamos al navegador y visitamos http://127.0.0.1:8000. ¡Listo! Tenemos una instalación de Laravel ejecutándose.

Resumiendo

Instalar y comenzar a utilizar Laravel es bastante sencillo una vez que tienes configurado PHP localmente y has instalado Composer. Después de eso, puedes instalar el instalador de Laravel y usar el comando serve para ejecutar el proyecto. Ya puedes empezar a añadir funcionalidades a tu aplicación.

]]>
tag:oliverservin.com,2005:World::Post/git 2023-03-10T00:00:00+00:00 2023-03-10T00:00:00+00:00 <![CDATA[Comprendiendo y utilizando Git para el desarrollo de software]]>

Git es un sistema que permite llevar un registro de cambios producidos a un conjunto de archivos.

Inicializando un repositorio Git

Por ejemplo, abre un directorio en tu sistema local con un editor de texto como VS Code.

A continuación, ejecuta init desde la línea de comandos.

git init

Acabas de crear un repositorio Git o un repo. Vive en el directorio oculto git y se encarga de dar seguimiento a todos los cambios que se produzcan en los archivos.

Creando instantáneas

A medida que trabajes en un código base, generarás instantáneas o commits del estado actual de los archivos. Cada commit tiene un ID único y está vinculado a un commit padre. Esto hace que podamos viajar en el tiempo a una versión anterior de nuestros archivos.

Puedes observar cómo se ilumina nuestro icono del control de versión.

CleanShot 2023-03-13 at 13.15.44@2x.png

Por ahora, todos nuestros archivos están sin seguimiento porque primero tenemos que añadirlos al repositorio.

Ejecuta add para hacer que se incluyan estos archivos al repositorio.

git add .

Ahora, genera una instantánea del estado actual de los archivos, ejecutando git commit junto con un mensaje sobre los cambios que se hicieron a los archivos.

git commit -m "init"

Bravo, acabas de crear tu primer commit en la cabecera de la rama o branch main del repositorio. Los archivos modificados han desaparecido y ahora estás en un directorio de trabajo limpio, sin modificaciones.

La cabecera o head representa el commit más reciente realizado. Si hacemos algunos cambios y hacemos un commit al repositorio, la cabecera avanzará, pero seguimos teniendo como referencia a nuestro commit anterior para que siempre podamos volver a él.

Pero algo que ocurre con el software, es que se desarrolla de forma no lineal. Es posible que varios equipos de trabajo colaboren simultáneamente en distintas funcionalidades dentro de un mismo código base. Git hace que esto sea posible mediante ramificaciones.

Ramificaciones o branches

Al ejecutar el comando branch puedes generar una rama.

git branch alternate-universe

Y para moverte a esta rama o branch ejecuta checkout.

git checkout alternate-universe

Ahora puedes trabajar con seguridad en alguna funcionalidad dentro de esta branch, sabiendo que no afectará el código o los archivos de la branch principal.

Los commits que hagas en esta branch vivirán en un universo paralelo con un historial propio.

Sin embargo, probablemente en algún momento quieras combinar este historial con el historial de la branch principal. Cuando esto suceda, puedes volver a la branch principal ejecutando checkout.

git checkout main

A continuación, ejecuta merge en tu universo paralelo.

git merge alternate-universe

La punta de tu branch de funcionalidad se convierte ahora en la cabecera de la branch principal.

En otras palabras, nuestro universo que antes teníamos fragmentado se ha convertido en uno solo. A menos que te hayas encontrado con un conflicto de incorporación o merge conflict. En este caso, tendrás que esperar al siguiente artículo.

]]>
tag:oliverservin.com,2005:World::Post/lindentidad 2023-02-16T00:00:00+00:00 2023-02-16T00:00:00+00:00 <![CDATA[Personalizando las notificaciones y errores de Laravel]]>

Generalmente, se puede distinguir qué apps han sido creadas con Laravel con ver el diseño del email para las notificaciones que envían. Fondo gris azulado con un encuadre blanco. Y el botón de acción en gris.

Lo mismo pasa con las páginas de error.

El diseño no es malo, pero podría dar la impresión de que la app carece de una identidad al no distinguirse de entre otras apps.

Por lo que dediqué tiempo para personalizar las notificaciones y las páginas de error.

Personalizando las notificaciones

Mi primer intento fue hacer que las notificaciones se envíen como texto plano. Sin nada superfluo. Texto, el enlace de la acción como URL y nada más.

Sin embargo, revisando la documentación, no encontré cómo cambiar de HTML a texto plano. Parecía que todas las notificaciones se envían en las dos versiones, HTML y en texto plano.

Buscando si alguien más tenía este problema, encontré un hilo y un post de @igorsantos07 en Laracasts con una posible solución. Parecía que creando una clase helper BareMail se podría hacer. Pero no me funcionó y no entendí el argumento de cómo funcionaría.

Después de mi fracaso, me di cuenta de que en vez de querer enviar emails en texto plano, lo que quería realmente era simplificarlos. Si seguía utilizando HTML, podría tener ventajas como la de controlar el tamaño de texto y sobre todo, mostrar enlaces, en vez de la URL completa. Muy útil para la URLs firmadas que son bastante largas.

Con el comando de artisan php artisan vendor:publish --tag=laravel-notifications pude publicar las vistas y hacer ajustes al diseño con el siguiente resultado.

Personalizando las páginas de error

Personalizar las páginas de error, es igual de sencillo. Con el comando de artisan php artisan vendor:publish --tag=laravel-errors se publican las vistas. No realicé un grandes cambios, sólo darles un poco de identidad.

]]>
tag:oliverservin.com,2005:World::Post/beacon 2023-02-11T00:00:00+00:00 2023-02-11T00:00:00+00:00 <![CDATA[Alternativa "low tech" al "Beacon" de Helpscout]]>

Una de las partes más esenciales en mis proyectos es poder recibir feedbak rápido por parte de mis usuarios. Ya sea para reportar algún bug o sugerir alguna mejora.

Revisando qué soluciones utilizan otras webs encontré que buena parte de ellas utilizan la funcionalidad de Beacon de Helpscout. Es un pequeño botón que se coloca en la parte inferior de la app y abre un pop up para poder enviar un mensaje de soporte a través de Helpscout.

Desafortunadamente, el precio de partida para emplear este servicio de USD$20 por mes. Una cuota no necesariamente accesible para apps hechas para desarrolladores independientes como yo.

Me pregunté, qué tan difícil sería implementar algo similar, solamente con lo esencial.

Lo que me llevó a crear un botón con icono de ayuda en la parte inferior derecha de mi app. Haciendo clic en el botón, se abre un modal que muestra un formulario de contacto. Al enviarse el formulario recibo un email con el mensaje del usuario y su dirección de email para poder contestarle vía email.

Una solución un poco rudimentaria, pero que les facilita a mis usuarios enviarme cualquier tipo de feedback. Y ya que soy el único que realiza soporte, es bastante práctico responder por email. Sin ninguna plataforma o solución que me añada fricción.

]]>
tag:oliverservin.com,2005:World::Post/dark 2023-02-04T00:00:00+00:00 2023-02-04T00:00:00+00:00 <![CDATA[Modo Dark con Tailwind CSS y Alpine.js]]>

Una tendencia que existe últimamente, entre los web apps, es la introducción de un theme dark. Y ahora que estoy desarrollando Olidisco quería intentar implementarlo también.

Tailwind es mi framework CSS de trabajo y cuenta con una variante dark para dar estilos cuando el modo dark está activo.

Por ejemplo, para cambiar el color del fondo de blanco a un gris muy oscuro y el texto de gris oscuro a blanco podemos utilizar el siguiente fragmento.

<div class="bg-white dark:bg-gray-900">
  <p class="text-gray-900 dark:text-white">
    Almacenamiento de imágenes en OliDisco.
  </p>
</div>

Para poder ver el cambio de estilos a dark, nuestro sistema debe tener activo el modo dark.

Activar manualmente la variante dark

Probablemente, queramos activar manualmente la versión dark independientemente de la configuración establecida en nuestro sistema.

Por defecto, Tailwind emplea la estrategia media para el modo dark. Que es usar la configuración del sistema. Pero podemos cambiarlo al modo class para que el modo dark se active cuando la clase dark este presente en nuestro HTML.

En nuestro archivo tailwind.config.js tendríamos lo siguiente.

module.exports = {
  darkMode: 'class',
  // ...
}

Y en nuestro HTML aplicamos la clase dark.

<html class="dark">
<body>
  <div class="bg-white dark:bg-gray-900">
    <!-- ... -->
  </div>
</body>
</html>

Switch para activar modo dark

Para activar el modo dark de forma dinámica podemos utilizar un poco de Javascript para crear un botón switch que lo active y lo desactive.

En mis proyectos uso el framework de Javascript Alpine. Por lo que el siguiente fragmento es para registrar globalmente darkMode con los métodos para activar o desactivar el modo dark.

<button
    type="button"
    x-data=""
    @click="$store.darkMode.inactive()"
>
    Light
</button>

<button
    type="button"
    x-data=""
    @click="$store.darkMode.active()"
>
    Dark
</button>

<script>
    document.addEventListener('alpine:init', () => {
        Alpine.store('darkMode', {
            active() {
                localStorage.theme = 'dark'

                document.documentElement.classList.add('dark')
            },

            inactive() {
                localStorage.theme = 'light'

                document.documentElement.classList.remove('dark')
            },
        })
    })
</script>

Haciendo clic en el botón dark, se establece la variable theme con el valor dark en el almacenamiento local y añade la clase dark a la etiqueta html.

Haciendo clic en el botón light, se establece la variable theme con el valor light en el almacenamiento local y quita la clase dark a la etiqueta html.

Hacer persistente el modo dark

Aunque hayamos activado la versión dark, la preferencia no es persistente si refrescamos la página.

Para hacerlo persistente podemos emplear el siguiente fragmento de Javascript para leer la variable theme y, si el valor es dark establecer la clase dark en el HTML. De lo contrario, quitarlo.

<script>
    if (localStorage.theme === 'dark') {
        document.documentElement.classList.add('dark')
    } else {
        document.documentElement.classList.remove('dark')
    }
</script>

Respetar la preferencia del sistema

Aunque ahora podemos activar o desactivar el modo dark, qué tal si queremos que se respete la preferencia del sistema. Es decir, que cuando el sistema tenga el modo dark activo, también activar la versión dark de la app.

Remplazamos el fragmento anterior con el siguiente, para utilizar el almacenamiento local o si no existe utilizar la preferencia del sistema.

<script>
    if (localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
        document.documentElement.classList.add('dark')
    } else {
        document.documentElement.classList.remove('dark')
    }
</script>

Ahora falta cambiar nuestro fragmento Alpine para añadir también un botón que restablezca la variable theme del almacenamiento local y leer la preferencia del sistema.

<button
    type="button"
    x-data=""
    @click="$store.darkMode.inactive()"
>
    Light
</button>

<button
    type="button"
    x-data=""
    @click="$store.darkMode.active()"
>
    Dark
</button>

<button
    type="button"
    x-data=""
    @click="$store.darkMode.reset()"
>
    Sistema
</button>

<script>
    document.addEventListener('alpine:init', () => {
        Alpine.store('darkMode', {
            active() {
                localStorage.theme = 'dark'

                document.documentElement.classList.add('dark')
            },

            inactive() {
                localStorage.theme = 'light'

                document.documentElement.classList.remove('dark')
            },

            reset() {
                localStorage.removeItem('theme')

                if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
                    document.documentElement.classList.add('dark')
                } else {
                    document.documentElement.classList.remove('dark')
                }
            }
        })
    })
</script>

Detectar el cambio de sistema a modo Dark

En Mac OS, si tenemos la preferencia del modo dark en automático, la versión dark se activará cuando haya anochecido y se desactivará al amanecer.

Para poder cambiarlo dinámicamente, utilizamos el siguiente fragmento para Alpine que añade el método init y poder escuchar cuándo el sistema haya activado o desactivado el modo dark.

<script>
    document.addEventListener('alpine:init', () => {
        Alpine.store('darkMode', {
            init() {
                window.matchMedia('(prefers-color-scheme: dark)').onchange = function() {
                    if (localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
                        document.documentElement.classList.add('dark')
                    } else {
                        document.documentElement.classList.remove('dark')
                    }
                }
            },

            active() {
                localStorage.theme = 'dark'

                document.documentElement.classList.add('dark')
            },

            inactive() {
                localStorage.theme = 'light'

                document.documentElement.classList.remove('dark')
            },

            reset() {
                localStorage.removeItem('theme')

                if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
                    document.documentElement.classList.add('dark')
                } else {
                    document.documentElement.classList.remove('dark')
                }
            }
        })
    })
</script>

Modo dark completado

Con esto, finalmente hemos implementado el modo dark en una app tomando en cuenta todos los casos puntuales:

  • Activar manualmente el modo dark
  • Switch para activar o desactivar el modo dark
  • Restablecer el modo dark a las preferencias del sistema
  • Detectar automáticamente cuándo el sistema haya cambiado el modo dark

El código completo se encuentra en el siguiente repositorio de Github.

]]>
tag:oliverservin.com,2005:World::Post/paquete-php 2022-09-13T00:00:00+00:00 2022-09-13T00:00:00+00:00 <![CDATA[Creando paquetes para PHP]]>

La creación de paquetes es una de las mejores formas para volver a aprovechar código y emplearlo en más de una app o proyecto. También hace que las apps sean más mantenibles. Si arreglamos un bug en nuestro paquete, con composer update actualizamos las apps que lo tengan como dependencia.

Creemos un paquete de ejemplo para revisar el proceso que conlleva la creación y publicación de paquetes PHP. Su funcionalidad será realizar conversiones de unidades de temperatura entre Fahrenheit y Celsius.


Plantilla "package-skeleton-php"

Spatie tiene una plantilla llamada "package-skeleton-php" que nos ofrece un buen punto de inicio. Cuenta con Composer configurado y algunas acciones de Github para ejecutar tests automáticamente y realizar revisiones de estilo en nuestro código base.

El repositorio Github de "package-skeleton-php" cuenta con un botón para comenzar a utilizar la plantilla.

Utilizar plantilla

En la página de creación de repositorio, elegimos el propietario del repositorio, un nombre y una descripción. Finalmente decidimos si la visibilidad del proyecto será pública o privada. Por el momento elegimos privada.

Crear repositorio

Clonamos el repositorio recién creado con el comando git clone:

git clone git@github.com:setdeu/fahrenheit-celsius-conversions.git

La plantilla nos ofrece un script de configuración que realiza los ajustes necesarios para asignar un autor, proveedor y nombre al paquete:

php ./configure.php

Nos pregunta el nombre, email y usuario de Github del autor. Un nombre y un namespace para el proveedor (vendor). Nombre del paquete, un nombre de la clase para el paquete y descripción.

Configurar plantilla

Revisemos qué archivos tenemos en nuestro repositorio a partir de la plantilla ya configurada.

  • .github/ISSUE_TEMPLATE — La plantilla que Github empleará cuando alguien cree una issue.
  • .github/workflows/dependabot-auto-merge.yml — Una acción de Github para hacer un merge automáticamente cuando el bot dependabot encuentre problemas de seguridad en nuestras dependencias npm.
  • .github/workflows/php-cs-fixes.yml — Una acción de Github para ejecutar una revisión de estilos al código base.
  • .github/workflows/run-tests.yml — Una acción de Github para ejecutar la suite de tests.
  • .github/workflows/update-changelog.yml — Una acción de Github para mantener actualizado nuestro archivo CHANGELOG.md en cada versión lanzada.
  • .github/workflows/dependabot.yml — Configuración del bot dependabot.
  • .github/FUNDING.yml — Configuración de Github para aceptar donaciones.
  • src/FahrenheitCelsiusConversionsClass.php — La clase PHP generada por la plantilla al inicializar nuestro paquete.
  • tests/ExampleTest.php — Un test de ejemplo para la suite de tests.
  • tests/Pest.php — Configuración para el framework de tests Pest.
  • .editorconfig — Configuración para los editores de texto que abran el proyecto.
  • .gitattributes — Configuración de Git para ignorar archivos que genera, por ejemplo, la ejecución de la suite de tests.
  • .gitignore — Configuración de Git para ignorar archivos de sistema comunes.
  • .php-cs-fixer.dist.php — Configuración para que haya un estándar de estilo en el código base.
  • .CHANGELOG.md — Información de los cambios que habrá en nuevas versiones del paquete.
  • composer.json — Configuración Composer con detalles del paquete.
  • LICENSE.md – Detalles de la licencia que tiene nuestro paquete. Inicialmente una licencia MIT.
  • phpunit.xml.dist — Configuración de PHPUnit.
  • README.md — Detalles que deberían leerse sobre nuestro paquete e instrucciones de uso.

Convirtiendo Celsius a Fahrenheit

El paquete de ejemplo que estamos creando servirá para convertir unidades Celsius a Fahrenheit. Comencemos a añadir la lógica necesaria para la conversión.

Renombraré el archivo src/FahrenheitCelsiusConversionsClass.php a src/Temperature.php para la clase Temperature. El construct de esta clase aceptará $celsius. Tendrá un método toFahrenheit para hacer la conversión a grados Fahrenheit usando la fórmula F = (C * (9/5)) + 32. Y un método estático celsius para aceptar grados celsius e inicializar la clase:

<?php

namespace Setdeu\FahrenheitCelsiusConversions;

class Temperature
{
    protected float $celsius;

	public function __construct(float $celsius)
	{
        $this->celsius = $celsius;
	}

	public function toFahrenheit(): float
    {
        return ($this->celsius * (9/5)) + 32;
    }

    public static function celsius(float $celsius): self
    {
        return new static($celsius);
    }
}

Generamos un test para asegurarnos de que la conversión sea correcta. Renombraré el archivo tests/ExampleTest.php a src/TemperatureTest.php con el siguiente test de conversión de 100 grados celsius a 212 grados Fahrenheit:

<?php

use Setdeu\FahrenheitCelsiusConversions\Temperature;

test('can convert celsius to fahrenheit', function () {
    $fahrenheit = Temperature::celsius(100)->toFahrenheit();

    expect($fahrenheit)->toEqual(212);
});

Ejecutamos el script test en Composer para verificar que pase el test:

composer test
Ejecución del script test

Ejecutando tests con Github Actions

Ejecutar automáticamente los tests, cada vez que el repositorio recibe un push de código o al crear un pull-request, es bastante útil para mantener el paquete estable.

De hecho, la plantilla que hemos utilizado incluye un workflow de Github para hacerlo posible. Se encuentra en el archivo .github/workflows/run-tests.yml:

name: Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      fail-fast: true
      matrix:
        os: [ubuntu-latest, windows-latest]
        php: [8.0]
        stability: [prefer-lowest, prefer-stable]

    name: P${{ matrix.php }} - ${{ matrix.stability }} - ${{ matrix.os }}

    steps:
      - name: Checkout code
        uses: actions/checkout@v3

      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: ${{ matrix.php }}
          extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick, fileinfo
          coverage: none

      - name: Setup problem matchers
        run: |
          echo "::add-matcher::${{ runner.tool_cache }}/php.json"
          echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json"

      - name: Install dependencies
        run: composer update --${{ matrix.stability }} --prefer-dist --no-interaction

      - name: Execute tests
        run: vendor/bin/pest

Para describir en qué momento ejecutar los tests, definimos on: [push, pull_request]. Así, al hacer un push de código o al generar un pull-request Github ejecutará el workflow.

En la sección matrix, se define una matriz para ejecutar el workflow mediante una combinación de variables:

  • os — Define los sistemas operativos que queremos ejecutar, la última versión de Ubuntu y la última versión de Windows.
  • php — Define las versiones de PHP que queremos ejecutar, PHP 8.0 en este caso, pero podemos añadir PHP 8.1 con [8.0, 8.1].
  • stability — Define las versiones de paquetes debe emplear Composer. Con Composer podemos preferir instalar las últimas versiones disponibles de los paquetes con prefer-stable o la versión mínima que se puede instalar con prefer-lowest.

En la sección steps, se definen los pasos para ejecutar los tests para cada combinación de variables en la matriz.

  • Checkout code — Define la creación e inicialización de un contenedor para los tests.
  • Setup PHP — Instala la versión de PHP definida en la matriz e instala una serie de extensiones comunes para PHP.
  • Setup problem matchers — Configura Github Actions para proporcionar errores cuando los tests fallen.
  • Install dependencies — Instala las dependencias con Composer con la estabilidad definida en la matriz.
  • Execute tests — Comando para iniciar la ejecución de los tests con Pest.

Al realizar un commit y push, y dirigirmos a la sección Actions del repositorio, observaremos que el workflow de tests se ha ejecutado.

Ejecución del workflow test

En la página de detalles, observamos que los tests se han ejecutado cuatro veces. Una vez por cada combinación posible. Y en el paso Execute tests vemos el detalle de que los tests fueron exitosos.

Ejecución del test

Si hacemos que nuestro test falle y hacemos un commit. Github Actions mostrará el error de que el test a fallado.

<?php

use Setdeu\FahrenheitCelsiusConversions\Temperature;

test('can convert celsius to fahrenheit', function () {
   $fahrenheit = Temperature::celsius(100)->toFahrenheit();
   $fahrenheit = Temperature::celsius(200)->toFahrenheit();

    expect($fahrenheit)->toEqual(212);
});
Test fallido

Revertimos el último cambio para que el test apruebe.

Pull requests

Intentemos la ejecución de los tests en un pull-request. Creamos una branch nueva, git checkout -b test-pr. Hacemos que el test falle y hacemos un commit.

Al crear un Pull request, Github nos mostrará que los tests están fallando. De esta manera podemos verificar que la suite de tests pasen incluso antes de incorporar el código al repositorio.

Test fallido en pull-request

Arreglando el estilo de código con Github Actions

Localmente, podemos arreglar problemas de estilo con el comando composer format, proporcionado por la plantilla que hemos usado. Ejecuta PHP CS Fixer utilizando el archivo de configuración .php-cs-fixer.dist.php.

composer format

Pero también podemos arreglar el estilo de código automáticamente en cada push al repositorio empleando Github Actions. Así, aseguramos una consistencia de estilo, incluso al recibir contribuciones de código.

La plantilla incluye un workflow de Github para hacerlo posible. Se encuentra en el archivo .github/workflows/php-cs-fixer.yml.

name: Check & fix styling

on: [push]

jobs:
  php-cs-fixer:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v3
        with:
          ref: ${{ github.head_ref }}

      - name: Run PHP CS Fixer
        uses: docker://oskarstark/php-cs-fixer-ga
        with:
          args: --config=.php-cs-fixer.dist.php --allow-risky=yes

      - name: Commit changes
        uses: stefanzweifel/git-auto-commit-action@v4
        with:
          commit_message: Fix styling

Inicializa un contenedor para ejecutar las tareas, ejecuta PHP CS Fixer para arreglar el estilo y realiza un commit "Fix styling" de los cambios realizados.

Al hacer un commit y push, y dirigirmos a la sección Actions del repositorio, observaremos que el workflow de Check & fix styling se ha ejecutado.

Ejecución del workflow Check & fix styling

Lanzamiento del paquete

Lancemos el paquete para que otros usuarios o proyectos puedan instalarlo vía Composer. Lo publicaremos en Packagist, que es el principal repositorio público de paquetes en el ecosistema PHP.

Comencemos con hacer público el repositorio para que Packagist pueda acceder a él.

En la sección de Settings de la página de nuestro repositorio en Github hay una sección para cambiar la visibilidad a público.

Repositorio settings Cambiar visibilidad a público

Ahora, con una cuenta registrada en Packagist enviamos nuestro paquete especificando la URL del repositorio.

Enviar a Packagist

Nuestro paquete ha sido publicado con una única versión dev-main. Es decir, Composer instalará el paquete con siempre hasta el último commit de la branch main.

Paquete publicado

Normalmente, etiquetaremos los lanzamientos por versión. Asegurándonos de que la versión etiquetada es estable para utilizarse.

Modifiquemos nuestro archivo CHANGELOG.md para dar detalles de la versión que estamos por lanzar. Este archivo contendrá nuestro historial de versiones.

Changelog v1.0.0

En Github creemos una nueva realease. Elegimos o generamos una tag. Colocamos la versión 1.0.0 como título para nuestra realease y una descripción.

Crear nueva realease Crear tag Crear realease

Creada nuestra release, Packagist automáticamente detectará la nueva versión y la hará disponible públicamente.

Nueva versión en Packagist

Usando el paquete publicado

Probemos instalar nuestro paquete.

Nos creamos un nuevo directorio de trabajo test:

mkdir test
cd test

Instalamos el paquete vía Composer:

composer require setdeu/fahrenheit-celsius-conversions

Creamos un archivo index.php para realizar una conversión de 100 grados Celsius a Fahrenheit.

<?php

include 'vendor/autoload.php';

use Setdeu\FahrenheitCelsiusConversions\Temperature;

echo Temperature::celsius(100)->toFahrenheit() . PHP_EOL;

Ejecutamos php index.php en nuestro terminal.

php index.php

Fantástico, hemos empleado nuestro paquete y hecho una conversión de prueba.


Publicando una nueva versión

Agreguemos y publiquemos una nueva funcionalidad al paquete para también pueda convertir grados Fahrenheit a celsius.

El construct de Temperature también aceptará $fahrenheit. Tendrá un método toCelsius para hacer la conversión a grados Celsius usando la fórmula C = (F - 32) * (5 / 9). Y un método estático fahrenheit para aceptar grados Fahrenheit e inicializar la clase:

class Temperature
{
    protected float $celsius;
    protected float $fahrenheit;

    public function __construct(float $celsius)
    public function __construct(float $celsius = 0, float $fahrenheit = 0)
    {
        $this->celsius = $celsius;
        $this->fahrenheit = $fahrenheit;
    }

    public function toFahrenheit(): float
        return ($this->celsius * (9 / 5)) + 32;
    }

    public function toCelsius(): float
    {
        return ($this->fahrenheit - 32) * (5 / 9);
    }

    public static function celsius(float $celsius): self
    {
        return new static($celsius);
    }

    public static function fahrenheit(float $fahrenheit): self
    {
        return new static(0, $fahrenheit);
    }
}

Generamos un test para asegurarnos de que la conversión sea correcta. En src/TemperatureTest.php añadimos el siguiente test de conversión de 212 grados Fahrenheit a 100 grados Celsius:

// ...

test('can convert fahrenheit to celsius', function () {
    $celsius = Temperature::fahrenheit(212)->toCelsius();

    expect($celsius)->toEqual(100);
});

Ejecutando nuestros tests verificamos que todo esté correcto.

composer test
composer test

Realizamos un commit y hacemos push al repositorio. También podemos comprobar en los Actions de Github estén pasando los tests.

Github actions

Para publicar una nueva version, comenzamos con actualizar el archivo CHANGELOG.md para dar detalles de la versión que estamos por publicar.

Changelog para 1.1.0

La versión será 1.1.0, esto es porque seguiremos las convenciones del versionado Semantic Versioning. Donde incrementos del último dígito 1.0.x representan arreglo de bugs. Incrementos del segundo dígito 1.x.0 representan nuevas funcionalidades añadidas sin introducir cambios de ruptura o breaking changes. Incrementos del primer dígito x.0.0 representan versiones que incluyen cambios de ruptura.

Creamos una nueva realease en Github:

Creando realease 1.1.0 Crear realease 1.1.0

Con esto, Packagist recogerá nuestra última versión publicada y la hará disponible públicamente.

Realease 1.1.0 en Packagist

Probemos nuestra nueva funcionalidad en nuestro proyecto de prueba.

Ejecutamos composer update para actualizar nuestras dependencias y obtener las últimas versiones.

composer update

En nuestro archivo index.php realizamos una conversión de 212 grados Fahrenheit a Celsius.

// ...

echo Temperature::fahrenheit(212)->toCelsius() . PHP_EOL;

Y ejecutamos php index.php en nuestro terminal.

composer update

Utilizando las issues y discussions de GitHub

Las funcionalidades de issues y discussions de GitHub son una excelente forma de estar abierto a discusiones y contribuciones. En nuestro repositorio contamos con un archivo .github/ISSUE_TEMPLATE/config.yml que configura cómo debe abrirse una issue en Github.

blank_issues_enabled: false
contact_links:
    - name: Ask a question
      url: https://github.com/setdeu/fahrenheit-celsius-conversions/discussions/new?category=q-a
      about: Ask the community for help
    - name: Request a feature
      url: https://github.com/setdeu/fahrenheit-celsius-conversions/discussions/new?category=ideas
      about: Share ideas for new features
    - name: Report a security issue
      url: https://github.com/setdeu/fahrenheit-celsius-conversions/security/policy
      about: Learn how to notify us for sensitive bugs
    - name: Report a bug
      url: https://github.com/setdeu/fahrenheit-celsius-conversions/issues/new
      about: Report a reproducable bug

Con la opción de blank_issues_enabled se deshabilita la creación de una issue en blanco. En su lugar, se mostrarán enlaces. Los primeros cuatro enlaces son para abrir un tema de discusión. La última, para crear una issue si se está reportando un bug.

Plantilla issue
  • Hacer una pregunta. La gente puede hacernos cualquier pregunta abriendo una nueva discusión.
  • Solicitar una funcionalidad. La gente puede sugerirnos añadir una nueva funcionalidad abriendo una nueva discusión.
  • Reportar un problema de seguridad. Esto lleva a la página de seguridad donde se pide a la gente que envíe un email en vez de hacer pública algún problema potencial de seguridad.
  • Reportar un bug. Crear una issue donde la gente puede hacernos saber de algún bug con información de cómo reproducirlo.

En desarrollo...

]]>
tag:oliverservin.com,2005:World::Post/laravel-9 2022-09-12T00:00:00+00:00 2022-09-12T00:00:00+00:00 <![CDATA[Principales novedades en Laravel 9]]>

Laravel fue lanzado el 8 febrero de 2022 e incluye múltiples mejoras y nuevas funcionalidades. Agrupación de rutas por controlador, una nueva API para definir accesores y mutadores, un nuevo motor de base de datos para Laravel Scout, y más mejoras.


Agrupando rutas por controlador

Supongamos que tenemos algunas rutas configuradas en nuestro archivo routes/web.php para una web de podcast.

Route::get('/podcasts', [PodcastController::class, 'index']);
Route::get('/podcasts/{podcast}', [PodcastController::class, 'show']);
Route::post('/podcasts', [PodcastController::class, 'store']);

En Laravel 9 podemos ahora agrupar las rutas por controlador con Route::controller.

Route::controller(PodcastController::class)->group(function () {
	Route::get('/podcasts', 'index');
	Route::get('/podcasts/{podcast}', 'show');
	Route::post('/podcasts', 'store');
});

Donde asumirá que el controlador será PodcastController para cualquier ruta declarada dentro del grupo.

Podemos probar las rutas con el comando php artisan route:list.

Lista de rutas

De hecho, el resultado del comando artisan route:list ha sido rediseñado también en Laravel 9. En versiones anteriores lucía así.

Lista de rutas v8

Clases de migración anónimas

Desde la versión 9, las clases de migraciones han dejado de tener nombre.

// migrations/2014_10_12_000000_create_users_table.php

return new class extends Migration
{
	// ...
}

Anteriormente, tendrían el nombre class CreateUsersTable. Incluso las migraciones generadas con el comando Artisan make:migration serán con una clase anónima.

php artisan make:migration create_podcasts_table
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up()
    {
        Schema::create('podcasts', function (Blueprint $table) {
            $table->id();
            $table->timestamps();
        });
    }

    public function down()
    {
        Schema::dropIfExists('podcasts');
    }
};

Nuevos helpers

Laravel 9 introduce dos nuevos helpers. str() y to_route().

Helper str()

str() es un nuevo helper para generar un objeto Stringable. Escencialmente es un alias de Str::of(). Por ejemplo, Str::of('Oliver')->upper() y str('Oliver')->upper() devuelven lo mismo. De esta manera, evitamos tener que importar la clase Str cada vez que queramos manipular un string.

use Illuminate\Support\Str;

Str::of('Oliver')->upper(); // OLIVER

str('Oliver')->upper(); // OLIVER

Helper to_route()

to_route es un nuevo helper para realizar redirecciones de una forma más limpia. Por ejemplo, redirect()->route('welcome') y to_route('welcome') redirigen a la misma ruta con nombre welcome.

// ...

return redirect()->route('welcome');

return to_route('welcome');

Rediseño de la página de error

La página de error de Ignition ha sido rediseñada para Laravel 9. Con un nuevo menú superior para acceder al stack, el contexto, la documentación y configuración de ignition. Quizá más intuitiva ahora.

Ignition en v9

En Laravel 8, lucía así.

Ignition en v8

Renderizar un string con Blade

Con el método Blade::render(), ahora es posible convertir un string con sintaxis Blade a su equivalente en HTML. Por ejemplo, Blade::render('Hola {{ $user->name }}', ['user' => auth()->user()]) devolverá "Hola Oliver".

// ...

return Blade::render('Hola {{ $user->name }}', ['user' => auth()->user()]);

// Hola Oliver

Scoped bindings forzados

Cuando vinculamos múltiples modelos Eloquent a una misma ruta podemos hacerlos de tal manera que el segundo modelo de Eloquent pertenezca al primer modelo con el método scopeBindings. Por ejemplo, en la siguiente ruta, buscará y devolverá el podcast únicamente si pertenece al usuario en la ruta. De lo contrario devolverá un error 404 de que no fue encontrado.

use App\Models\Podcast;
use App\Models\User;
 
Route::get('/users/{user}/podcasts/{podcast}', function (User $user, Podcast $podcast) {
    return $podcast;
})->scopeBindings();

Cobertura de tests

Laravel 9 introduce una nueva opción --coverage al comando Artisan test. Habilitando esta opción nos genera un reporte de la cobertura que tienen nuestros tests en el proyecto.

Esta opción requiere que tengamos instalado XDebug. En Mac y con Homebrew instalado, podemos hacerlo con el comando pecl install xdebug. Modificamos nuestro archivo php.ini para añadir la configuración XDEBUG_MODE=coverage y reiniciamos PHP.

Ahora podemos ejecutar el comando Artisan php artisan test --coverage para ejecutar los tests y generarnos un reporte. En este caso la cobertura es del 54.2%.

Cobertura

Si queremos establecer una cobertura mínima en nuestro proyecto, podemos hacerlos con la opción --min, php artisan test --coverage --min=70 De esta manera nos arrojará error si no cumplimos con la mínima cobertura.

Cobertura

Motor "base de datos" para Laravel Scout

Para bases de datos pequeñas, Laravel ahora incluye un motor "base de datos" para usarlo junto con Laravel Scout. De esta manera, no tenemos que utilizar o instalar un servicio como Algolia o MeiliSearch.

Para utilizar este motor, simplemente debemos añadir la variable SCOUT_DRIVER=database en nuestro archivo .env.

Hay que tomar en cuenta que este motor únicamente funciona para bases de datos MySQL y Postgres.


Índices Full Text

En bases de datos MySQL o PostgreSQL podemos definir columnas del tipo fullText para generar índices Full Text:

$table->text('description')->fullText();

De esta manera podemos realizar consultas del tipo MATCH AGAINST, en el caso de MySQL:

$podcasts = Podcast::whereFullText('description', 'Laravel')->get();

Bastante útil para realizar búsquedas de texto simples. Aporta un mejor rendimiento que con consultas del tipo where like.


Nueva API para accesores y mutadores

Ahora podemos definir nuestro accesores y mutadores de una forma más limpia.

Anteriormente, para definir un accesores que nos devuelva el valor en mayúsculas y un mutador que guarde el valor en minúsculas utilizábamos la convención getXAttribute y setXAttribute:

public function getUsernameAttribute($value)
{
	return str($value)->upper();
}

public function setUsernameAttribute($value)
{
	return str($value)->lower();
}

Ahora, podemos hacerlo con un solo método que declare que devolveremos un objeto Illuminate\Database\Eloquent\Casts\Attribute.

use Illuminate\Database\Eloquent\Casts\Attribute;

// ...

public function username(): Attribute
{
	return new Attribute(
		get: fn($value) => str($value)->upper(),
		set: fn($value) => str($value)->lower()
	);
}
]]>
tag:oliverservin.com,2005:World::Post/sitios-forge 2022-09-10T00:00:00+00:00 2022-09-10T00:00:00+00:00 <![CDATA[Cómo crear un sitio en Forge]]>

Con un servidor app o web aprovisionado por Forge, podemos crear y administrar sitios web fácilmente y de forma ilimitada.

En la configuración de sitios hay un sitio que Forge genera automáticamente en cada aprovisionamiento. Este sito default es el que responderá cuando accedamos al servidor por su dirección IP. Podemos eliminarlo y a su vez comenzará a devolver un error 404.

Sitios Forge

En cualquier momento podemos recrear el sitio default. Simplemente, necesitamos crear un sitio nuevo e introducimos default en el campo Dominio Raíz (Root Domain).

Algo a considerar es que Forge crea un segundo sitio y que está oculto en la lista de sitios. Es un sitio especial que se encarga de cerrar la conexión cuando el servidor recibe solicitudes de dominios que no están configurados en el servidor. Esta es una medida de seguridad para evitar actos maliciosos de dominios que no son nuestros.

En el formulario de creación de sitio colocamos nuestro nombre de dominio. Este también será el directorio raíz donde Forge clonará nuestro código. También podemos indicar _ aliases_ de dominio. Los _ aliases_ sería nombres de dominio o subdominios que responderán también a este sitio. Útil en aplicaciones multi-tenantes.

Crear sitio

Elegiremos el tipo de proyecto para nuestro sitio. Con Forge no solo podemos desplegar sitios en Laravel, también pueden ser proyectos PHP en general. Dejaré la opción de Laravel marcada.

El Directorio Web (Web Directory) representa el directorio donde nuestro archivo index.php o index.html reside dentro de nuestro código. En una aplicación Laravel, lo usual es que sea el directorio /public.

Podemos activar la opción de Allow wildcard subdomain para permitir que cualquier subdominio responda al sitio que estamos creando,

Si marcamos la opción de aislamiento de sitio web (Use Website Isolation), Forge creará un PHP fpm pool separado para el sitio. Esto creará un usuario de sistema único que se encargará de ejecutar el sitio. Realizando un aislamiento de PHP entre varios sitios.

Por últimos, durante la creación del sitio, también podemos indicar que se cree una base de datos. Generalmente, nuestros proyectos Laravel usarán una.

Con el sitio creado, Forge ahora nos permite instalar un repositorio. Aunque también podemos inicializar Wordpress o phpMyAdmin.

Elegir repositorio

Seleccionamos instalar un repositorio Github. Indico el nombre de mi repositorio Laravel y selecciono la branch a instalar. La opción de instalar dependencias Composer (Install Composer Dependencies) debe estar activada para que Forge inicialice las dependencias de nuestro proyecto.

Instalación de repositorio

Cuando Forge instala un repositorio Git, lo hace haciendo un clone del tipo shallow y en la branch que le hemos indicado. Esto para que la instalación y despliegue de nuestra aplicación sea veloz. Especialmente en repositorios grandes con bastantes commits. También significa que, el repositorio clonado en el servidor, únicamente existirán los últimos 50 commits realizados y una única branch.

Si nos dirigimos a la configuración de Ambiente, Forge nos ha creado y configurado el archivo .env basándose en el archivo .env.example que existe en el repositorio.

Configuración de Ambiente ]]>
tag:oliverservin.com,2005:World::Post/servidor-forge 2022-09-09T00:00:00+00:00 2022-09-09T00:00:00+00:00 <![CDATA[Creando un servidor en Laravel Forge]]>

Veamos cómo crear un servidor en Laravel Forge paso a paso. Desde la configuración de un proveedor de servidores, hasta el despliegue de una aplicación web.


Configurando un proveedor de servidores

En la pestaña de servidores o Servers, hacemos clic en crear servidor o Create Server. Nos abre un modal con cinco proveedores de servidor. DigitalOcean, Linode, AWS, Vultr y Hetzner Cloud.

En una cuenta nueva, los proveedores estarán deshabilitados. Para comenzar el proceso de alta, hacemos clic en algún proveedor. En este caso daré de alta Vultr.

Selección de proveedor

En el formulario de nuevo proveedor (New Provider), tendremos seleccionado Vultr como proveedor y debemos añadir nuestras credenciales. Colocamos "Personal" como el nombre de perfil (Profile Name).

Alta de Vultr

Creando un servidor

Con nuestro proveedor activo, podemos de nuevo ir a la pestaña Servers para crear un servidor.

Al seleccionar Vultr como proveedor, nos muestra un formulario para el tipo y especificaciones del servidor que queremos crear.

Con Forge podemos configurar siete tipos de servidor.

  • App Server
  • Web Server
  • Worker Server
  • Database Server
  • Cache Server
  • MeiliSearch Server
  • Load Balancer
Tipo de servidor

En este caso elegiré "App Server". Incluye "php", "nginx", "database", "redis" y "memcached". Prácticamente, todo lo que necesitamos para una aplicación web.

Elegimos un nombre para el servidor. Forge automáticamente nos genera uno, pero podemos cambiarlo. La región donde queremos que sea aprovisionado el servidor. El tamaño del servidor. En Vultr ofrece 15 opciones. Elegimos la versión de PHP que queremos instalar. Desde PHP 5.6 y hasta PHP 8.1.

Opciones de servidor

Ya que estamos instalando un App Server, debemos elegir el servidor de base de datos a instalar. MySQL, MariaDB o Postgres.

Opciones de base de datos

Al instalar el servidor de base de datos, Forge nos creará una primera base de datos. Por defecto se llamará "forge" pero podemos cambiarlo.

Finalmente, hacemos clic en crear servidor o Create Server.


El sitio default

Después de completada la creación de servidor, Forge nos dirigirá a la sección de Sitios (Sites). En la parte inferior existe una bloque de Sitios Activos (Active Sites). Habrá por defecto un sitio llamado "default".

Sitio default

Forge instalará este sitio por defecto en cualquier creación de servidor. Es el sitio que Forge utilizará cuando accedemos a nuestro servidor utilizando la dirección de IP.

Configurando un repositorio Git

Haciendo clic en el sitio "default", nos llevará a la configuración de sitio. Podemos elegir una App para el sitio. Puede ser un Repositorio Git, Wordpress o phpMyAdmin. En este caso haré clic en Repositorio Git (Git Repository).

Sitio default 2

Como aún no hemos configurado un proveedor Git como Github, la única opción que nos muestra es usar un proveedor personalizado. Vamos a la configuración de cuenta para conectar nuestra cuenta de Github.

Ir a conectar Github Conectar Github

Volviendo a nuestro sitio "default", podemos elegir Github y en este ejemplo instalará Laravel laravel/laravel. Elegiré instalar la branch 9.x. Corresponde a la última versión de Laravel. Y seleccionaré instalar las dependencias Composer.

Instalar Laravel]

Haciendo clic en Instalar Repositorio (Install Repository) para instalarlo.

Ahora nuestro primer sitio ha sido desplegado. Si visitamos el servidor por la dirección de IP, podemos ver que Forge a reemplazado nuestra aplicación temporal por una aplicación Laravel.

Cuando Forge realiza despliegues, lo que hace es utilizar un Script de Despliegue (Deploy Script) que indica qué comandos ejecutar durante los despliegues.

Deploy Script

Ambiente del Sitio

En la página de configuración de Ambiente "Environment", Forge nos muestra el contenido del archivo .env de nuestra aplicación web. Podemos modificarlo sin tener que acceder a nuestro servidor vía SSH o SFTP y realizar manualmente los ajustes. Esto es bastante práctico.

Por ejemplo, podemos cambiar el nombre de nuestra aplicación a "LaraForge" y guardamos los cambios. Desde la configuración de App, ejecutamos un despliegue nuevo de nuestra aplicación web para utilizar el nuevo nombre.

Ambiente del Sitio ]]>
tag:oliverservin.com,2005:World::Post/faker-pest 2022-09-08T00:00:00+00:00 2022-09-08T00:00:00+00:00 <![CDATA[Cómo usar Faker en Pest]]>

Pest nos ofrece un plugin que nos permite acceder a Faker de forma más funcional.

Instalando Faker

El plugin lo instalamos vía Composer.

composer require pestphp/pest-plugin-faker --dev

Usando Faker

Para utilizar Faker, simplemente debemos invocar la función faker().

<?php

use function Pest\Faker\faker;

test('application', function () {
    $user = [
        'name' => faker()->name,
        'email' => faker()->email,
        'phone' => faker()->e164PhoneNumber,
        'country' => faker()->randomElement(['mx', 'us']),
        'postal_code' => faker()->postcode,
    ];
});

faker() también acepta que le pasemos un argumento $locale. De esta manera, usamos Faker con otro locale diferente al de por defecto. Así, nos puede devolver valores en otro idioma como el español.

<?php

use function Pest\Faker\faker;

test('application', function () {
    $user = [
        'name' => faker('es_MX')->name
    ];
});
]]>
tag:oliverservin.com,2005:World::Post/expectativas-pest 2022-09-06T00:00:00+00:00 2022-09-06T00:00:00+00:00 <![CDATA[Expectativas en Pest]]>

Pest proporciona una API de expectations o expectativas. Una API limpia, declarativa y extensible para hacer verificaciones.

Inicializamos la API de expectativas al declarar el helper expect(). Y le pasamos un valor a verificar, expect($value).

Por ejemplo, para comprobar que true es verdadero, usamos la expectativa toBeTrue().

<?php

test('application', function () {
    expect(true)->toBeTrue();
});

Expectativas disponibles

Supongamos que queremos validar los valores de un usuario.

Para comprobar que el nombre del usuario sea un string toBeString(). Y que no sea un valor vacío not->toBeEmpty(). Anteponemos la propiedad not para decirle a la API que queremos lo opuesto a la expectativa.

<?php

test('valid name', function () {
	$user = App\Models\User::factory()->create();

	expect($user->name)->toBeString()->not->toBeEmpty();
});

toContain('@') para verificar que el email del usuario sea un email válido, podemos esperar que contenga el símbolo @.

<?php

test('valid email', function () {
	$user = App\Models\User::factory()->create();

	expect($user->email)->toBeString()->toContain('@');
});

toStartWith('+') para verificar que el teléfono del usuario sea un teléfono válido, podemos esperar que comience con el símbolo + para números internacionales.

<?php

test('valid phone', function () {
	$user = App\Models\User::factory()->create();

	expect($user->phone)->toBeString()->toStartWith('+');
});

toBe('admin') para verificar que el valor sea exactamente uno establecido.

<?php

test('admin role', function () {
	$user = App\Models\User::factory()->create();

	expect($user->role)->toBe('admin');
});

toBeIn(['mx', 'us']) para verificar que el valor sea alguno de los establecidos.

<?php

test('valid country', function () {
	$user = App\Models\User::factory()->create();

	expect($user->country)->toBeIn(['mx', 'us']);
});

En la documentación de Pest se listan todas las expectativas que hay disponibles.


Expectativas personalizadas

Algunas veces nos gustaría tener expectativas personalizadas. Haciendo que nuestros tests sean más expresivos. Por ejemplo, una expectativa toBePhoneNumber(), en vez de toBeString()->toStartWith('+').

Pest nos permite hacerlo extendiendo la API de expectativas. Así, podemos añadir expectativas personalizadas que mejor se adapten al proyecto que estamos trabajando.

Editamos el archivo de configuración tests/Pest.php. Dentro de la sección "Expectativas" o "Expectations" comenzamos con un llamado a la funcion expect() sin ningua propiedad. Invocamos el método extend() y le pasamos el nombre del macro que vamos a crear, toBePhoneNumber. Añadimos un closure como segundo parámetro con la lógica para nuesta expectativa personalizada.

<?php

// tests/Pest.php

// ...

expect()->extend('toBePhoneNumber', function ($areaCode) {
	expect($this->value)->toBeString()->toStartWith('+');
});

Si hay alguna propiedad, como areaCode, que quisiéramos añadir a nuestra expectativa, podemos aceptarla en el closure.

<?php

// ...

expect()->extend('toBePhoneNumber', function ($areaCode) {
	// ...
});

Expectativas de orden superior

Con las expectativas de orden superior de Pest, podemos simplificar los test drásticamente al pasar simplemente el objecto principal al método expect.

Por ejemplo, pasar el objecto del usuario y realizar expectativas sobre las propiedades del objecto.

<?php

test('application', function () {
	expect(App\Models\User::factory()->create())
		->name->toBeString()->not->toBeEmpty()
		->email->toBeString()->toContain('@')
		->phone->toBePhoneNumber()
		->role->toBe('admin');
		->country->toBeIn(['mx', 'us']);
});

Haciendo esto, nos deja un test mucho más limpio y claro.

]]>
tag:oliverservin.com,2005:World::Post/login-helper 2022-09-05T00:00:00+00:00 2022-09-05T00:00:00+00:00 <![CDATA[Helper para iniciar sesión en tests Pest]]>

Iniciar sesión como un usuario es algo muy usual que hacemos en nuestros tests. Principalmente en endpoints donde la creación de modelos deben estar asociados al usuario que los haya creado. Por ejemplo, el autor de una entrada de blog $post->author.

Una forma de hacerlo es utilizar el método actingAs que Laravel proporciona.

<?php

test('application', function () {
    $this->actingAs(App\Models\User::factory()->create());
});

Pero sería bueno abstraer esta lógica en un helper que sea más declarativo. Por ejemplo login(). En Pest podemos hacerlo definiendo test helpers en el archivo tests/Pest.php.

Haremos que al helper se le puede pasar un usuario, de lo contrario generaremos uno. Para poder acceder a nuestro TestCase fuera de una clase debemos utilizar el test() helper. Esto nos devuelve el TestCase actual de dónde se haya llamado nuestra función login.

<?php

// tests/Pest.php

// ...

function login($user = null)
{
    return test()->actingAs($user ?? App\Models\User::factory()->create());
}

Ahora podemos utilizar nuestro helper en los tests.

<?php

test('application', function () {
    login();
});
]]>
tag:oliverservin.com,2005:World::Post/testing-hooks-pest 2022-09-04T00:00:00+00:00 2022-09-04T00:00:00+00:00 <![CDATA[Testing Hooks en Pest]]>

Al trabajar en nuestros tests Pest, frecuentemente nos encontraremos con querer ejecutar un mismo código de preparación para múltiples tests. Por ejemplo, contar con un usuario ya creado en varios tests. En estos casos, Pest nos ofrece utilizar testing hooks.

Hooks beforeAll, beforeEach, afterEach y afterAll

Pest nos proporciona 4 testing hooks: beforeAll, beforeEach, afterEach y afterAll. Útiles para reutilizar un mismo preparativo a múltiples tests antes de ejecutarlos.

Creamos un test de prueba tests/Unit/PestHooksTest.php para mostrar el uso de estos testing hooks.

beforeAll(fn () => var_dump('Antes de todo'));
beforeEach(fn () => var_dump('Antes de cada uno'));
afterEach(fn () => var_dump('Después de cada uno'));
afterAll(fn () => var_dump('Después de todo'));

test('verdadero es verdadero', function () {
    $this->assertTrue(true);
});

test('falso es falso', function () {
    $this->assertFalse(false);
});

Ejecutamos nuestra prueba con artisan test y usamos el argumento filter para ejecutar solamente el test PestHooksTest.

artisan test --filter=PestHooksTest

El resultado nos devuelve lo siguiente.

string(13) "Antes de todo"
string(17) "Antes de cada uno"
string(20) "Después de cada uno"
string(17) "Antes de cada uno"
string(20) "Después de cada uno"
string(16) "Después de todo"

beforeAll se ha ejecutado al comienzo una sola vez. beforeEach y afterEach se han ejecutado dos veces. Al comenzar cada una de nuestros dos tests y al finalizar. afterAll se ha ejecutado al final una sola vez.

Algo a tomar en cuenta, es que los hooks beforeAll y afterAll no nos dan acceso a las funcionalidades de Laravel. Devolverá un error si intentamos acceder a Laravel desde estos hooks porque Laravel aún no ha sido inicializado en estos hooks.

Por ejemplo, si tenemos lo siguiente.

beforeAll(fn () => var_dump(config('app.name')));
afterAll(fn () => var_dump(config('app.name')));

test('verdadero es verdadero', function () {
    $this->assertTrue(true);
});

test('falso es falso', function () {
    $this->assertFalse(false);
});

Nos devolverá error.

Error en Pest Hook

Función uses

Pest también nos ofrece la función uses() para cuando queremos usar traits en nuestros tests.

Por ejemplo, para usar el trait RefreshDatabase. Un trait muy común que restablece nuestra base de datos antes de cada test.

use Illuminate\Foundation\Testing\RefreshDatabase;

uses(RefreshDatabase::class);

test('verdadero es verdadero', function () {
    $this->assertTrue(true);
});

test('falso es falso', function () {
    $this->assertFalse(false);
});

Así restablecemos la base de datos antes de ejecutar cada uno de nuestros dos tests.

El trait LazilyRefreshDatabase es otra opción para restablecer la base de datos. A diferencia de RefreshDatabase, LazilyRefreshDatabase no resetea la base de datos al ejecutar cada test. Si no que solamente la restablece hasta que un test use la base de datos. Ayudando a mejorar el rendimiento de los tests que no necesitan interactuar con la base de datos.

Este trait es perfecto para emplearlo en nuestro tests/TestCase.php base. Así, nos olvidamos de tener que usar RefreshDatabase en los archivos de test.

namespace Tests;

use Illuminate\Foundation\Testing\LazilyRefreshDatabase;
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;

abstract class TestCase extends BaseTestCase
{
    use CreatesApplication;
    use LazilyRefreshDatabase;
}
]]>
tag:oliverservin.com,2005:World::Post/instalando-pest 2022-09-03T00:00:00+00:00 2022-09-03T00:00:00+00:00 <![CDATA[Cómo instalar Pest en un proyecto Laravel]]>

Pest es un Framework de testing para PHP enfocado en ser simple pero funcional. Creado por Nuno Maduro.

Cuenta con un plugin para Laravel. Lo cual nos garantiza su integración.

Comencemos con la instalación y configuración de Pest en un proyecto Laravel existente.


Instalación

Lo primero es instalar Pest mediante composer.

composer require pestphp/pest --dev --with-all-dependencies

Ya que estamos utilizando Laravel, podemos instalar el plugin específico que Pest nos proporciona.

composer require pestphp/pest-plugin-laravel --dev

Ejecutamos el comando Artisan pest:install para instalar Pest. Esto nos genera un archivo tests/Pest.php que indica cómo ejecutar o extender Pest según las especificaciones de nuestro proyecto.

php artisan pest:install

Tests básicas

Ahora que hemos instalado Pest, creemos algunos tests básicos. Así, visualizaremos cómo luce un test de Pest.

Primero una Unit test básica en Unit/ExampleText.php para aseverar que true es true.

test('that true is true', function () {
    $this->assertTrue(true);
});

Ahora, una Feature test básica en Feature/ExampleText.php para aseverar que nuestra aplicación devuelve una respuesta exitosa.

test('the application returns a successful response', function () {
    $response = $this->get('/');

    $response->assertStatus(200);
});

Ejecutar Pest

Finalmente, es momento de ejecutar nuestros Tests de Pest con el comando Artisan test.

php artisan test
Ejecutando Pest ]]>
tag:oliverservin.com,2005:World::Post/one 2022-08-09T00:00:00+00:00 2022-08-09T00:00:00+00:00 <![CDATA[Un side project puede cambiar tu vida]]>

Hace trece años publiqué una plantilla para Wordpress llamada wpClassifieds. Una plantilla que facilitaba la creación de webs de clasificados. Relativamente popular en su momento. Llegó a ser utilizado por cientos, quizá miles de personas.

Cambió mi futuro. Porque lo más importante llegó años después. Llamó la atención del creador de Yclas, anteriormente Open Classifieds. Que además de una amistad que aún perdura, también me invitó a trabajar con él. En un trabajo remoto. Un trabajo que me ha permitido viajar por el mundo. Y que a pesar de haber cambiado de dueño hace un año, sigue permitiéndome conocer gente nueva en todo el mundo.

Sin embargo, eso no lo vemos cuando pensamos en crear algo. Normalmente, nos quedamos solo pensando, teorizando o planeando. Incluso creamos excusas para no hacerlo. Lo que al final importa es hacerlo sin más. Dar clic en publicar. Sin expectativas. Sin buscar la perfección.

Se trata de tener una idea y de actuar. Hacer algo y mostrarlo al mundo. Quizá solo una persona llegue a apreciarlo. Otras veces, cientos de personas. Haberlo hecho, valdrá completamente la pena.

]]>
tag:oliverservin.com,2005:World::Post/internet 2022-08-01T00:00:00+00:00 2022-08-01T00:00:00+00:00 <![CDATA[El Internet de las plataformas sociales]]>

Cuando el Internet llegó a casa, comenzó un nuevo mundo para mi. Un lienzo en blanco. Uno en el que no había reglas. Ideal para explorar y también para experimentar.

Últimamente eso ha cambiado. Particularmente, ha desaparecido la impresión de ser el dueño de nuestras creaciones.

El nuevo mandamás es el algoritmo. El algoritmo de las plataformas sociales que nos gobiernan. A merced de sus reglas y de su autoridad.

Invertimos años construyendo una audiencia en sus plataformas. Pero es una audiencia que nos puede ser arrebatada en un segundo. Nuestra cuenta puede ser vetada o bloqueada. Sin previo aviso. Casi imposible de apelar. La audiencia que habíamos construido nunca fue realmente nuestra.

Las newsletters son quizá la única opción que tenemos para poseer una audiencia. Casi podemos garantizar ser leídos o escuchados. No sin excepciones. Gmail puede enterrar nuestro email. Mandándolo a la carpeta de Spam. O a la de promocionales. Y nunca ser visto por nuestros lectores.

¿Cómo garantizar no perder nuestra audiencia? Empleando herramientas que podamos controlar. Construyendo una marca o producto que enamore a la gente. Una que no olvidarán. Una que intentarían buscarlo por su cuenta si un día desaparece de su feed.

En el nuevo Internet hay que hacer un verdadero esfuerzo para ver de nuevo algo que nos había gustado. Porque usualmente solo se nos muestra una vez. Después es enterrado. Debajo de todo el contenido nuevo que se crea o recomienda.

Ahora parece que el mundo real es el mejor sitio para encontrar libertad de creación o nuevas posibilidades.

Vaya ironía. Este nuevo Internet comienza a sentirse limitante.

]]>
tag:oliverservin.com,2005:World::Post/php82 2022-07-19T00:00:00+00:00 2022-07-19T00:00:00+00:00 <![CDATA[Cómo instalar PHP 8.2 en una Mac]]>

PHP versión 8.2 está próxima a ser lanzada. Su lanzamiento está programado para el día 24 de noviembre del 2022.

Algo recomendable, es probar nuestras aplicaciones o paqueterías antes del lanzamiento oficial. Así nos anticipamos a posibles incompatibilidades.

Veamos cómo instalar una versión preview de PHP 8.2 en una Mac utilizando Brew.

Instalación de PHP 8.2 preview

Primero, nos aseguramos de tener Brew actualizado.

brew update

Instalamos el repositorio que contiene la fórmula de PHP 8.2.

brew tap shivammathur/php

Instalamos PHP 8.2 y lo inicializamos.

brew install php@8.2
brew link --overwrite --force php@8.2

Nos aseguramos de que el comando php esté ejecutando la última versión.

php -v

// PHP 8.2.0-dev (cli) (built: Jun 29 2022 00:22:19) (NTS)
// Copyright (c) The PHP Group
// Zend Engine v4.2.0-dev, Copyright (c) Zend Technologies
//    with Zend OPcache v8.2.0-dev, Copyright (c), by Zend Technologies

Ahora tenemos PHP 8.2 en nuestra Mac. Listo para realizar pruebas.

]]>
tag:oliverservin.com,2005:World::Post/fly 2022-06-24T00:00:00+00:00 2022-06-24T00:00:00+00:00 <![CDATA[Desplegando Laravel en Fly.io]]>

Fly.io nos permite desplegar web apps en 21 regiones en todo el mundo. Reduciendo la latencia entre usuario y app.

Hacer un despliegue de cualquier app desarrollada en Laravel es bastante sencillo.

Instalación y registro en Fly.io

Instalamos la línea de comandos flyctl con Homebrew. brew install flyctl. Regístrate a fly.io si aún no lo estás. flyctl auth signup.

Configurar Laravel para Fly.io

Comenzamos con un proyecto Laravel nuevo. laravel new hola-fly-laravel. Lanzamos flyctl launch que nos configura la App para Fly. Le asignamos un nombre. Elegimos una organización. Una región para desplegarla. La primera opción será la región más cercana que nos ha detectado.

El programa nos preguntará por nuestra APP_KEY. Podemos generar una si aún no la tenemos. Lanzamos php artisan key:generate --show en una nueva ventana de nuestro terminal. Cuando pregunte si queremos realizar un despliegue, respondemos no.

Abrimos nuestro proyecto con nuestro editor. Vemos que se han generado los archivos fly.toml, Dockerfile y la carpeta docker. Para configurar la URL de nuestra app, modificamos el archivo de configuración fly.toml. Fly nos genera una URL con base en el nombre de nuestra App. hola-fly-laravel.fly.dev.

[env]
  LOG_LEVEL = "info"
  APP_URL = "hola-fly-laravel.fly.dev"

Desplegando

Finalmente desplegaremos nuestra App. fly deploy. Voilà. Nuestra App Laravel está desplegada. Podemos abrirla en nuestro navegador con fly open.

PHP 8.1

El archivo Dockerfile nos ha desplegado PHP 8.0. Pero sería bueno tener PHP 8.1

Modificamos Dockerfile y reemplazamos toda coincidencia de php8 a php81. También eliminaremos php8-pecl-mcrypt. Una extensión obsoleta en PHP 8.1. Y añadimos un enlace simbólico para ejecutar php81 como php. ln -s /usr/bin/php81 /usr/bin/php. Realizamos los siguientes ajustes a los siguientes archivos.

  • docker/app.conf: cambiar la referencia de /var/run/php/php8-fpm.sock a /var/run/php/php81-fpm.sock.
  • docker/php-fpm.conf: cambiar la referencia de include=/etc/php8/php-fpm.d/*.conf a include=/etc/php81/php-fpm.d/*.conf
  • docker/server.conf: cambiar la referencia de /var/run/php/php8-fpm.sock a /var/run/php/php81-fpm.sock
  • docler/supervisor.conf: cambiar la referencia de command=php-fpm8 -R --nodaemonize a command=php-fpm81 -R --nodaemonize

Volvemos a hacer el despliegue. fly deploy. Nuestra App ahora corre sobre PHP 8.1

Scheduler

También podemos ejecutar tareas programadas. Modificamos docker/supervisor.conf y descomentamos la sección [program:laravel-schedule].

Gracias a Fly.io, tenemos Laravel desplegado en una región cercana a nosotros. Corriendo la última versión de PHP y ejecutando tareas programadas.

]]>
tag:oliverservin.com,2005:World::Post/pint 2022-06-23T00:00:00+00:00 2022-06-23T00:00:00+00:00 <![CDATA[Probando Laravel Pint]]>

Laravel Pint es un nuevo paquete del ecosistema Laravel. Creado por Nuno Maduro. Permite corregir el estilo y mantener la consistencia de nuestro código base.

Instalación

Para instalarlo únicamente requerimos de composer. composer require laravel/pint --dev. Así lo añadimos como una dependencia de desarrollo a nuestro proyecto. Nos instala un archivo binario sin dependencias.

Ejecutando Pint

./vendor/bin/pint. Y arreglará el estilo de nuestro proyecto con el estándar PSR-12.

Pint también cuenta con un conjunto de reglas preestablecidas para Laravel. pint --preset laravel. Ideal para proyectos Laravel.

Otra forma que recomiendo ejecutarlo es como un script en Composer. composer format. Para esto añadimos el evento de comando format en el composer.json de nuestro proyecto

{
    "scripts": {
        "test": "vendor/bin/pint --preset laravel"
    }
}
]]>
tag:oliverservin.com,2005:World::Post/consentimiento-paddle 2022-01-14T00:00:00+00:00 2022-01-14T00:00:00+00:00 <![CDATA[Gestionar el consentimiento de marketing de Paddle con Laravel]]>

En Paddle, un comprador puede optar por recibir emails de marketing y es almacenado como un miembro nuevo de audiencia.

Pero, al emplear un event listener puedes gestionar el consentimiento de marketing en Paddle con Laravel Cashier.

En el siguiente ejemplo empleo el atributo de usuario subscribed, pero también puedes usar un servicio externo o herramienta como Mailcoach de Spatie y tenerlo sincronizado.

<?php

namespace App\Listeners;

use App\Models\User;
use Laravel\Paddle\Events\WebhookReceived;

class PaddleEventListener
{
    public function handle (WebhookReceived $event)
    {
        if ($event->payload['alert_name' ] === 'new_audience member') {
            if ($user = User::whereEmail($event->payload['email'])->first()) {
                $user->fill([
                    'subscribed' => $event->payload['marketing_consent'],
                ])->save();
            }
        }

        if ($event->payload['alert_name'] === 'update_audience_member') {
            if ($user = User::whereEmail($event->payload['new_customer_email'])->first()) {
                $user->fill([
                    'subscribed' => $event->payload['new_marketing_consent'],
                ])->save();
            }
        }
    }
}
]]>
tag:oliverservin.com,2005:World::Post/stripe-sync-es 2022-01-14T00:00:00+00:00 2022-01-14T00:00:00+00:00 <![CDATA[Cómo mantener en sincronía los detalles de cliente con Stripe en Laravel]]>

Existe un útil método syncStripeCustomerDetails disponible en Laravel Cashier para Stripe que te permite mantener en sincronía los detalles de tus clientes.

De esta manera, cuando un cliente actualiza su email o nombre también se actualizarán en Stripe.

// App/Models/User.php

namespace App\Models;

use function Illuminate\Events\queueable;

class User extends Authenticatable
{
    // ...

    // listen for the updated User model event and invoke `syncStripeCustomerDetails`

    protected static function booted()
    {
        static::updated(queueable(function ($customer) {
            $customer->syncStripeCustomerDetails();
        }));
    }
}

Código en acción:

Puedes sobrescribir los atributos utilizados para sincronizar la información al añadir los métodos stripeName, stripeEmail, stripePhone y stripeAddress

]]>
tag:oliverservin.com,2005:World::Post/boton-cashier 2022-01-12T00:00:00+00:00 2022-01-12T00:00:00+00:00 <![CDATA[Personalizar el diseño del botón de pago en Laravel Cashier Paddle]]>

El diseño del botón de pago al usar Laravel Cashier Paddle no es particularmente el más bonito. Afortunadamente Laravel Cashier ofrece un componente de blade paddle-button con la opción de añadir estilos personalizados.

Puedes añadir data-theme="none" al componente paddle-button para deshabilitar el diseño por defecto de Paddle:

<x-paddle-button :url="$payLink" data-theme="none">
    Buy Product
</x-paddle-button>

Después puedes añadir algunos estilos como Tailwind CSS para darle una mejor apariencia.

<x-paddle-button :url="$payLink" class="w-full bg-green-600 border border-transparent rounded-md py-3 px-8 flex items-center justify-center text-base font-medium text-white hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500" data-theme="none">
    Buy Product
</x-paddle-button>

+

]]>
tag:oliverservin.com,2005:World::Post/widget-paddle-es 2022-01-12T00:00:00+00:00 2022-01-12T00:00:00+00:00 <![CDATA[Incrustar el widget de pago de Paddle con Laravel Cashier]]>

Una opción para una integración más transparente con Paddle es incrustar el widget de pago dentro de una página de pago pre-existente. Esto puede reducir la sensación de estar dejando el sitio web para pagar.

Con Laravel Cashier es realmente muy sencillo. Incluye un componente de Blade llamado paddle-checkout y acepta un atributo override para pasar el enlace de pago generado con Cashier:

$payLink = $user->chargeProduct($productId);

// Blade view
<x-paddle-checkout :override="$payLink" />

Incluso podemos personalizar los colores del inline checkout desde el tablero de Paddle para tener una mejor integración con nuestro sitio web.

]]>
tag:oliverservin.com,2005:World::Post/reset-es 2022-01-11T00:00:00+00:00 2022-01-11T00:00:00+00:00 <![CDATA[Auto-resetear semanalmente una app demo en Laravel]]>

Trabajé en un proyecto donde necesitaba presentar una demo pública del trabajo en progreso pero necesitaba resetear el demo después de una semana al re-establecer la base de datos a su estado original.

Mi solución fue simplemente una scheduled task en el método schedule de la clase App\Console\Kernel.

namespace App\Console;

use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;

class Kernel extends ConsoleKernel
{
    /**
     * The Artisan commands provided by your application.
     *
     * @var array
     */
    protected $commands = [
        //
    ];

    /**
     * Define the application's command schedule.
     *
     * @param  \Illuminate\Console\Scheduling\Schedule  $schedule
     * @return void
     */
    protected function schedule(Schedule $schedule)
    {
        $schedule->command('migrate:fresh --force --seed')->weekly();
    }

    /**
     * Register the commands for the application.
     *
     * @return void
     */
    protected function commands()
    {
        $this->load(__DIR__.'/Commands');

        require base_path('routes/console.php');
    }
}

Las opciones de frecuencia están bien documentadas en Laravel docs. Puedes cambiar la frecuencia a diario o cada hora si lo prefieres.

]]>
tag:oliverservin.com,2005:World::Post/sanctum-es 2022-01-10T00:00:00+00:00 2022-01-10T00:00:00+00:00 <![CDATA[Eliminar la tabla personal_access_tokens de una App en Laravel]]>

Sanctum es ahora la API por defecto desde Laravel 8.6.0. Sin embargo, inicialmente la mayoría de mis proyectos no necesitan de esa API, pero debido a Sanctum termino con la table extra de personal_access_tokens.

Fácilmente puedes eliminar Sanctum con ejecutar composer remove y eliminando o comentando el endpoint api/user em tu archivo de rutas api.

composer remove laravel/sanctum
// routes/api.php

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;

// Route::middleware('auth:sanctum')->get('/user', function (Request $request) {
//     return $request->user();
// });

También necesitarás eliminar la migración CreatePersonalAccessTokensTable y ejecutar el comando migrate:fresh para limpiar todas las tablas de la base de datos y ejecutar la migración de nuevo.

rm database/migrations/2019_12_14_000001_create_personal_access_tokens_table.php
php artisan migrate:fresh
]]>
tag:oliverservin.com,2005:World::Post/freelance 2022-01-07T00:00:00+00:00 2022-01-07T00:00:00+00:00 <![CDATA[Tips para trabajar como desarrollador web freelancer]]>

El último par de años me he dedicado al trabajo freelance para proyectos de desarrollo web. No siempre es fácil, pero esto es lo que he aprendido como freelancer.

La mejor forma de encontrar clientes es que los clientes te encuentren a ti. Si compartes tips o ayudas a otras personas, también te haces notar y logras hacerte visible para que los clientes puedan encontrarte. Cuando los clientes te encuentran, ya te conocen un poco y tienen una idea de lo que pueden esperar de ti.

Antes de comenzar un proyecto reúnete con el cliente y solo acepta trabajos con lo que estarías cómodo trabajando. Si algún cliente no te da buena espina, es mejor rechazar el trabajo. Habrá otros clientes que buscan ayuda.

No abarates tu trabajo. Contratar a un freelancer es bastante barato para las compañías en comparación de un empleado.

No temas a los retos. En ocasiones encontrarás trabajos con tareas que no dominas o no has hecho antes. No debe ser un problema, eres un profesional y puedes asumir el reto y encontrar la mejor solución al problema.

Acuerda el primer pago lo antes posible. De esta manera puedes ver el tiempo que llega a tardar el cliente en hacer un pago, o si la facturación se hace correctamente, u otros posibles contratiempos de pago.

Solicita el 33 % de pago por adelantado si el proyecto tiene un presupuesto fijado. Así puedes ir solventando tus gastos como freelancer en lo que terminas de recibir el pago por completo.

Tener pocos clientes, pero con términos de larga duración o con presupuestos altos puede ser mejor. Tener que coordinar proyectos nuevos cada par de semanas es laborioso. Además, los proyectos largos pueden ayudarte a mejorar la relación a largo plazo entre cliente y freelancer.

No desarrolles para ti mismo. Puede ser que no siempre estés en el proyecto, intenta desarrollar pensando en lo que podría ser mejor para el cliente y el proyecto.

Trabajar con un freelancer puede ser difícil para las compañías. Generalmente por una cuestión de confianza, ya que no pueden verte trabajando. Intenta ser amable y que además tienes el interés de hacer lo mejor posible para el proyecto. Así logras ganar confianza.

Intenta dar actualizaciones sobre el avance del proyecto. Apreciarán saber en qué has estado trabajando y quizá posibles cambios a los requerimientos si es necesario.

Cuando el proyecto haya concluido, solicita un testimonial al cliente sobre cómo evalúa tu desempeño. Compartir experiencias de clientes pasados es muy útil para conseguir nuevos clientes.

No tengas miedo de dar por terminado el proyecto antes. En ocasiones puede suceder que tú o el cliente no compartan las mismas expectativas. Es mejor dar por concluido el proyecto con antelación y en los mejores términos posibles.

Nunca dejes de aprender. Siempre intenta mejorar tu forma de trabajo con proyectos y con clientes. Los errores o contratiempos son inevitables, pero es mejor aprender de ellos.

]]>
tag:oliverservin.com,2005:World::Post/api-object 2021-11-19T00:00:00+00:00 2021-11-19T00:00:00+00:00 <![CDATA[Interact with API result values as object properties in Laravel]]>

With the object method in the Laravel HTTP client, you can easily interact with API result values as object properties.

// https://api.adviceslip.com/advice response
// {
//   "slip": {
//     "id": 100,
//     "advice": "Everybody makes mistakes."
//   }
// }

$advice = Http::get('https://api.adviceslip.com/advice')->object();

$advice->slip->id; // 100
$advice->slip->advice; // Everybody makes mistakes.
]]>
tag:oliverservin.com,2005:World::Post/relative-route 2021-11-18T00:00:00+00:00 2021-11-18T00:00:00+00:00 <![CDATA[Get relative path with `route()` in Laravel]]>

I needed to generate a relative path, but the Laravel route helper generates absolute paths by default.

Turns out it also accepts a third $absolute argument, and setting it to false generates an relative path instead.

route('page.show', $page->id);
// http://laravel.test/pages/1

route('page.show', $page->id, false);
// /pages/1
]]>
tag:oliverservin.com,2005:World::Post/chokidar 2021-11-17T00:00:00+00:00 2021-11-17T00:00:00+00:00 <![CDATA[Chokidar file watcher to auto-run Pest tests]]>

When working with Pest tests, I usually use Chokidar file watcher to auto-run my test and have instant feedback after changing any code.

chokidar "app/**/*.php" "tests/**/*.php" -c "pest --filter 'work in progress test'"

You can install Chokidar CLI via npm:

npm install -g chokidar-cli
]]>
tag:oliverservin.com,2005:World::Post/chain-where 2021-11-16T00:00:00+00:00 2021-11-16T00:00:00+00:00 <![CDATA[Chaining "Where" conditions using dynamic methods]]>

Laravel Eloquent can chain "where" conditions using dynamic/magic methods to fetch entries:

Order::whereMethodAndStatus('card', 'paid')->get();

// before

Order::where('method' 'card')
    ->where('status', 'paid')
    ->get();

// after
Order::whereMethodAndStatus('card', 'paid') ->get();
]]>
tag:oliverservin.com,2005:World::Post/magic-methods 2021-11-13T00:00:00+00:00 2021-11-13T00:00:00+00:00 <![CDATA[Model factories with relationships by using magic methods]]>

When using factories with relationships, Laravel also provides magic methods:

User::factory()->hasPosts()->create();

Perhaps not the most IDE friendly option, but I think it feels more readable.

// magic factory relationship methods
User::factory()->hasPosts(3)->create();

// insted of
User::factory()->has(Post::factory()->count(3))->create();
]]>
tag:oliverservin.com,2005:World::Post/due-cashier 2021-11-12T00:00:00+00:00 2021-11-12T00:00:00+00:00 <![CDATA[Show amount to pay in next billing cycle with Laravel Cashier (Stripe)]]>

You can easily show how much a customer will pay in the next billing cycle with the upcomingInvoice method in Laravel Cashier (Stripe) to get the upcoming invoice details.

// routes/web.php

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;

// ...

Route::get('/profile/invoices', function (Request $request) {
    return view('/profile/invoices', [
        'upcomingInvoice' => $request->user()->upcomingInvoice(),
        'invoices' => $request-›user()->invoices(),
    ]);
});
]]>
tag:oliverservin.com,2005:World::Post/tax-stripe 2021-11-11T00:00:00+00:00 2021-11-11T00:00:00+00:00 <![CDATA[Tax calculation with Laravel Cashier for Stripe]]>

Dealing with Tax calculation on checkout pages always makes me sweat, especially EU Taxes. Luckily, Stripe and Laravel Cashier, in recent releases, have simplified it.

You only need to add calculateTaxes method in your AppServiceProvider, and it will automate the tax collection and calculation using Stripe Tax service.

// App\Providers\AppServiceProvider.php

use Laravel\Cashier\Cashier;

public function boot ()
{
    Cashier::calculateTaxes();
}

Stripe's payment page starts collecting and calculating taxes from your customers.

]]>
tag:oliverservin.com,2005:World::Post/sync-stripe 2021-11-10T00:00:00+00:00 2021-11-10T00:00:00+00:00 <![CDATA[How to keep in sync your customers details with Stripe in Laravel]]>

There is a handy syncStripeCustomerDetails method available on Laravel Cashier for Stripe that lets you keep in sync your customers details.

So, when a customer updates their email or name details, it gets updated on Stripe too.

// App/Models/User.php

namespace App\Models;

use function Illuminate\Events\queueable;

class User extends Authenticatable
{
    // ...

    // listen for the updated User model event and invoke `syncStripeCustomerDetails`

    protected static function booted()
    {
        static::updated(queueable(function ($customer) {
            $customer->syncStripeCustomerDetails();
        }));
    }
}

Code in action:

You can overwrite the attributes used to sync the information by adding the following methods stripeName, stripeEmail, stripePhone, and stripeAddress methods.

]]>
tag:oliverservin.com,2005:World::Post/consent 2021-11-09T00:00:00+00:00 2021-11-09T00:00:00+00:00 <![CDATA[Handle Paddle's marketing consent with Laravel]]>

On Paddle, a buyer can opt-in to receive marketing emails and is stored as a new audience member.

But, by using an event listener, you can handle the Paddle's marketing consent on Laravel Cashier too.

The following example code uses a subscribed user attribute, but you can use an external service or tool like Spatie's Mailcoach and have it synced.

<?php

namespace App\Listeners;

use App\Models\User;
use Laravel\Paddle\Events\WebhookReceived;

class PaddleEventListener
{
    public function handle (WebhookReceived $event)
    {
        if ($event->payload['alert_name' ] === 'new_audience member') {
            if ($user = User::whereEmail($event->payload['email'])->first()) {
                $user->fill([
                    'subscribed' => $event->payload['marketing_consent'],
                ])->save();
            }
        }

        if ($event->payload['alert_name'] === 'update_audience_member') {
            if ($user = User::whereEmail($event->payload['new_customer_email'])->first()) {
                $user->fill([
                    'subscribed' => $event->payload['new_marketing_consent'],
                ])->save();
            }
        }
    }
}
]]>
tag:oliverservin.com,2005:World::Post/pay-button 2021-09-05T00:00:00+00:00 2021-09-05T00:00:00+00:00 <![CDATA[Custom styling the pay button with Laravel Cashier Paddle]]>

When using Laravel Cashier Paddle, the styling provided for the pay button is not the prettier. Fortunately, Laravel Cashier provides a paddle-button blade component with the option to add custom styling.

Standard Paddle styling.

To disable the standard Paddle styling, you can add the data-theme="none" to the component:

<x-paddle-button :url="$payLink" data-theme="none">
    Buy Product
</x-paddle-button>

Then you can add some Tailwind CSS to give it a better look:

<x-paddle-button :url="$payLink" class="w-full bg-green-600 border border-transparent rounded-md py-3 px-8 flex items-center justify-center text-base font-medium text-white hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500" data-theme="none">
    Buy Product
</x-paddle-button>
]]>
tag:oliverservin.com,2005:World::Post/checkout-widget 2021-09-04T00:00:00+00:00 2021-09-04T00:00:00+00:00 <![CDATA[Embed Paddle's checkout widget with Laravel Cashier]]>

An option for a more transparent Paddle integration is to embed the Paddle's checkout widget within your existing checkout page. It can remove the sense of your users leaving your website to pay.

Buy button vs. Inline checkout.

Laravel Cashier makes it really simple. It includes a paddle-checkout Blade component, and accepts an override attribute to pass the pay link generated with Cashier:

$payLink = $user->chargeProduct($productId);

// Blade view
<x-paddle-checkout :override="$payLink" />

We can even customize the inline checkout branding colors within Paddle's dashboard to have a better integration on our website.

Paddle's branding options.

]]>
tag:oliverservin.com,2005:World::Post/reset 2021-09-02T00:00:00+00:00 2021-09-02T00:00:00+00:00 <![CDATA[Weekly auto-reset a Laravel demo app]]>

I have worked on a client's work where I needed to present a public demo of the work in progress App, but wanted to reset the demo after a week by restoring the database to its clean state.

I did it with a simply scheduled task in the schedule method of my application's App\Console\Kernel class.

namespace App\Console;

use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;

class Kernel extends ConsoleKernel
{
    /**
     * The Artisan commands provided by your application.
     *
     * @var array
     */
    protected $commands = [
        //
    ];

    /**
     * Define the application's command schedule.
     *
     * @param  \Illuminate\Console\Scheduling\Schedule  $schedule
     * @return void
     */
    protected function schedule(Schedule $schedule)
    {
        $schedule->command('migrate:fresh --force --seed')->weekly();
    }

    /**
     * Register the commands for the application.
     *
     * @return void
     */
    protected function commands()
    {
        $this->load(__DIR__.'/Commands');

        require base_path('routes/console.php');
    }
}

Frequency options are well documented on Laravel docs. You can change it to daily or hourly if you prefer.

]]>
tag:oliverservin.com,2005:World::Post/sanctum 2021-09-01T00:00:00+00:00 2021-09-01T00:00:00+00:00 <![CDATA[Remove the personal_access_tokens table on a fresh Laravel App]]>

Since Laravel v8.6.0, Sanctum is now the default API authentication stack. However, initially most of my projects don't need an API, but because of the Sanctum package I end up with an extra personal_access_tokens table.

You can easily remove Sanctum by running composer remove, and commenting out or remove the api/user endpoint in your api route file.

composer remove laravel/sanctum
// routes/api.php

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;

// Route::middleware('auth:sanctum')->get('/user', function (Request $request) {
//     return $request->user();
// });

You may also need to remove the `CreatePersonalAccessTokensTable` migration, and run the `migrate:fresh` command to drop all tables from the database and run migrate again.

rm database/migrations/2019_12_14_000001_create_personal_access_tokens_table.php
php artisan migrate:fresh
]]>
tag:oliverservin.com,2005:World::Post/umami 2021-08-27T00:00:00+00:00 2021-08-27T00:00:00+00:00 <![CDATA[Deploy Umami analytics with Launcher]]>

I have recently moved my deployment solution to Launcher and I wanted to use it to deploy Umami in my server too. It involves a few steps.

Create a new site and set the Git repository to git@github.com:mikecao/umami.git, and branch master, and copy the new database credentials.

Edit the App .env file.

DATABASE_URL=mysql://USERNAME:PASSWORD@mysql-8:3306/statsyourdomaincom
HASH_SALT=WzkHGhOkPkV5dQHwVmOXeJK7ro4uWL5Z

Replacing USERNAME and PASSWORD with your generated database credentials, and RANDOMSTRING with a random string.

On macOS, you can use the following code to generate a random string of 32 characters.

cat /dev/urandom | tr -dc 'a-zA-Z0-9' | head -c 32

Edit the Deploy script (entrypoint).

#!/bin/bash

set -eux -o pipefail
export NODE_OPTIONS=--max-old-space-size=8192

echo "Git pull"
git remote set-url origin $LAUNCHER_SITE_GIT_REPOSITORY
git pull origin $LAUNCHER_SITE_GIT_BRANCH

if [ -f yarn.lock ]; then
    echo "Install yarn dependencies and build production assets"
    yarn install
    yarn run build
fi

exec "$@"

Edit the Dockerfile PHP-FPM to expose the port 3000 used for Umami.

# https://hub.docker.com/u/uselauncher/php-fpm-80 # https://github.com/uselauncher/php-fpm-80 FROM uselauncher/php-fpm-80 # Uncomment the two lines below to install additional PHP extensions # See: https://github.com/mlocati/docker-php-extension-installer # USER root # RUN install-php-extensions ldap USER launcher EXPOSE 3000

Deploy your stats.yourdomain.com site.

Create a daemon with the following command.

yarn --cwd /app start

SSH into container to get the daemon container name.

ssh launcher@YOURSERVERIP -t 'sudo docker ps'

SSH into container to get the daemon container IP address.

ssh launcher@YOURSERVERIP -t 'sudo docker inspect stats.yourdomain.com-daemon-1ee049e0-9ec6-40ba-ac0a-c81da43098e1'

Edit your Nginx site config to proxy the traffic and add this location block as shown below, and redeploy the site.

location / {
     proxy_pass http://172.18.0.23:3000;
     proxy_set_header Host $host;
     proxy_set_header X-Real-IP $remote_addr;
     proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}

Now Umami is accesible at https://stats.yourdomain.com, and you can log in using admin/umami and change your password.

]]>
tag:oliverservin.com,2005:World::Post/launcher-storage 2021-08-21T00:00:00+00:00 2021-08-21T00:00:00+00:00 <![CDATA[Fix Laravel storage:link using Launcher]]>

Somehow, the php artisan storage:link command doesn't work as expected using the Launcher service, and files stored in local public disk are not publicly accessed.

To fix it, SSH into your site container and manually create the symbolic link.

ln -s ../storage/app/public/ public/storage

If public/storage already exists, you mean need to remove it first.

rm public/storage

Update:

It seems this is a known issue, and it has been addressed the `php artisan storage:link` command has been removed from the Deploy script (entrypoint) config file, and replaced with ln -s ../storage/app/public/ public/storage.

]]>
tag:oliverservin.com,2005:World::Post/launcher 2021-08-20T00:00:00+00:00 2021-08-20T00:00:00+00:00 <![CDATA[Launcher: first impressions]]>

My Laravel Forge subscription was about to expire, and regardless of being a happy customer, I was looking for a cheaper alternative. Having to pay $19 per month was a bit expensive for my little side projects.

A couple of weeks ago Launcher was announced by Pascal Baljet, and he gave me a beta invite, so I was happy to give it a try.

Launcher is a new service to deploy sites. It's kind of an alternative to Laravel Forge, but with the difference of having services and sites running in Docker containers, so every site and service is isolated within your VPS.

Currently, the service costs €4.99 per month, but it has purchasing power parity, so living in Mexico it costs around MX$55.00. A really good price that includes unlimited servers, unlimited sites, and unlimited deployments.

Deploying sites is effortless. Create a server, choose a repository, the PHP version, and you are ready to go. Currently, it supports Digital Ocean and Hetzner as cloud providers, but custom providers is a coming soon feature.

Launcher seems a great fit for my deployment needs, and I already moved all my side projects to it. Hopefully, support for more Cloud Providers, custom SSL certificates, and domain aliases features will be available soon.

]]>
tag:oliverservin.com,2005:World::Post/tiptap 2021-06-23T00:00:00+00:00 2021-06-23T00:00:00+00:00 <![CDATA[Integrating Tiptap in a Laravel-Livewire project]]>

Install Javascript dependencies.

npm install -D alpinejs@2 @tiptap/core @tiptap/starter-kit

Install Livewire.

composer require livewire/livewire

Initialize the editor.

For this example, we will initialize the editor in resources/js/app.js

require('./bootstrap');

require('alpinejs');

import { Editor } from '@tiptap/core'
import StarterKit from '@tiptap/starter-kit'

window.setupEditor = function() {
    return {
        editor: null,
        init(element) {
            this.editor = new Editor({
                element: element,
                extensions: [
                    StarterKit
                ],
                content: this.content,
                onUppublished_at: ({ editor }) => {
                    this.content = editor.getHTML()
                }
            })
        },
    }
}

And compile the assets:

npm run dev

Make a Blade component.

Create an anonymous Blade component for the editor at resources/views/components/editor.blade.php

<div x-data="{ content: @entangle($attributes->wire('model')), ...setupEditor() }" x-init="() => init($refs.editor)" wire:ignore {{ $attributes->whereDoesntStartWith('wire:model') }} > <div x-ref="editor"></div> </div>

Make a Livewire component.

Create a Livewire component for the editor.

php artisan make:livewire editor

Add a `content` property in the component.

namespace App\Http\Livewire;

use Livewire\Component;

class Editor extends Component
{
    public $content;

    public function render()
    {
        return view('livewire.editor');
    }
}

Render the editor Blade component in Livewire component.

<div>
    <x-editor wire:model="content"/>
</div>
]]>
tag:oliverservin.com,2005:World::Post/adsense 2021-03-11T00:00:00+00:00 2021-03-11T00:00:00+00:00 <![CDATA[Probando Google Ads con RadioCúbito(Design)]]>

Desde el pasado mes comencé un nuevo producto para promover mis servicios de diseño web, RadioCúbito(design). Un servicio de diseño web ilimitado mediante subscripción.

Para promover este nuevo servicio comencé una campaña en Google Ads para obtener tráfico y posibles leads. El presupuesto máximo que destiné fue de $2,300 por un mes. Con este presupuesto obtuve 162 clics y 2,860 impresiones.

Sin embargo, ninguno de estos clics se convirtieron en clientes. Hubo un sólo usuario que me contactó por email, pero sin más seguimiento posterior.

Esto me pone a dudar la efectividad de la publicidad de Google. Aunque otra opción es que los visitantes por Google Ads no entendieron o no les interesó la propuesta de RadioCúbito(Design).

Estas fueron las palabras clave utilizadas:

]]>
tag:oliverservin.com,2005:World::Post/ghost 2021-01-08T00:00:00+00:00 2021-01-08T00:00:00+00:00 <![CDATA[How to run a Ghost blog on Laravel Forge]]>

Since I have a Laravel Forge account to help me manage my server, I wanted to host this Ghost blog in it too, and it's relatively easy.

Create a server on Forge

I'm using a Stardust instance in Scaleway since it keeps my operating costs relatively low. It costs around $2.2/month(€0.0025/hour).

Create a new site with /ghost/current path as Web Directory, and set up a new SSL certificate with LetsEncrypt.

Setup Nginx

Edit the site Nginx configuration.

Replace index index.html index.htm index.php; with index index.js index.html index.htm index.php;

Replace the location block from:

location ~ \.php$ {
    fastcgi_split_path_info ^(.+\.php)(/.+)$;
    fastcgi_pass unix:/var/run/php/php7.4-fpm.sock;
    fastcgi_index index.php;
    include fastcgi_params;
}

to:

location / {
    proxy_pass http://127.0.0.1:2368;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection ‘upgrade’;
    proxy_set_header Host $host;
    proxy_cache_bypass $http_upgrade;
}

Add a MySQL database

Create a new database for Ghost.

Install Ghost

Connect to the server via SSH.

ssh forge@IP_ADDRESS

Install Ghost CLI.

sudo npm i -g ghost-cli

Remove your existing ghost directory.

cd oliver.mx
sudo rm -rf ghost

Make a ghost directory and install Ghost.

mkdir ghost && cd ghost    
ghost install

The installation process looks like this:

Enter your blog URL: https://oliver.mx
Enter your MySQL hostname: 127.0.0.1
Enter your MySQL username: forge
Enter your MySQL password: [Forge created Database password]
Enter your Ghost database name: ghostblog

# Answering no will skip the SSL set up as well
Do you wish to set up Nginx? n

Do you wish to set up "ghost" mysql user? n 

Do you wish to set up Systemd? Y

Do you want to start Ghost? Y

Congrats! Your new Ghost blog is alive.

]]>