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.
]]>Para comenzar a utilizar Laravel, necesitamos descargarlo e instalarlo. Pero, antes de eso, hay algunos pasos de configuración que debemos realizar.
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.
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.
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.
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.
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.
Git es un sistema que permite llevar un registro de cambios producidos a un conjunto de archivos.
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.
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.
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.
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.
]]>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.
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.
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.
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.
]]>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.
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>
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
.
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>
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>
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>
Con esto, finalmente hemos implementado el modo dark en una app tomando en cuenta todos los casos puntuales:
El código completo se encuentra en el siguiente repositorio de Github.
]]>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.
"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.
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.
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.
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.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
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.
Al realizar un commit y push, y dirigirmos a la sección Actions del repositorio, observaremos que el workflow de tests se ha ejecutado.
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.
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);
});
Revertimos el último cambio para que el test apruebe.
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.
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
.
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.
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.
Ahora, con una cuenta registrada en Packagist enviamos nuestro paquete especificando la URL del repositorio.
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.
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.
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.
Creada nuestra release, Packagist automáticamente detectará la nueva versión y la hará disponible públicamente.
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.
Fantástico, hemos empleado nuestro paquete y hecho una conversión de prueba.
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
Realizamos un commit y hacemos push al repositorio. También podemos comprobar en los Actions de Github estén pasando los tests.
Para publicar una nueva version, comenzamos con actualizar el archivo CHANGELOG.md
para dar detalles de la versión que estamos por publicar.
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:
Con esto, Packagist recogerá nuestra última versión publicada y la hará disponible públicamente.
Probemos nuestra nueva funcionalidad en nuestro proyecto de prueba.
Ejecutamos composer update
para actualizar nuestras dependencias y obtener las últimas versiones.
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.
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.
En desarrollo...
]]>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.
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
.
De hecho, el resultado del comando artisan route:list
ha sido rediseñado también en Laravel 9. En versiones anteriores lucía así.
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');
}
};
Laravel 9 introduce dos nuevos helpers. str()
y to_route()
.
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
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');
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.
En Laravel 8, lucía así.
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
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();
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%.
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.
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.
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
.
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()
);
}
]]>
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
.
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.
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.
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.
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.
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.
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.
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).
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.
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.
Ya que estamos instalando un App Server, debemos elegir el servidor de base de datos a instalar. MySQL, MariaDB o Postgres.
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.
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".
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.
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).
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.
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.
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.
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.
Pest nos ofrece un plugin que nos permite acceder a Faker de forma más funcional.
El plugin lo instalamos vía Composer.
composer require pestphp/pest-plugin-faker --dev
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
];
});
]]>
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();
});
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.
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) {
// ...
});
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.
]]>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();
});
]]>
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.
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.
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;
}
]]>
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.
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
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);
});
Finalmente, es momento de ejecutar nuestros Tests de Pest con el comando Artisan test
.
php artisan test
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.
]]>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.
]]>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.
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.
]]>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.
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
.
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"
Finalmente desplegaremos nuestra App.
fly deploy
.
Voilà.
Nuestra App Laravel está desplegada.
Podemos abrirla en nuestro navegador con fly open
.
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
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.
]]>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.
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.
./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"
}
}
]]>
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();
}
}
}
}
]]>
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
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>
+
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.
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.
]]>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
]]>
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.
]]>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.
]]>
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
]]>
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
]]>
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();
]]>
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();
]]>
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(),
]);
});
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.
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.
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();
}
}
}
}
]]>
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>
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.
]]>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.
]]>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
]]>
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.
]]>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
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
.
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.
]]>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>
]]>
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:
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.
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.
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;
}
Create a new database for 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.
]]>