Cosas de Laravel

Algunos tips de Laravel

Crear una Clase Helper para un proyecto Laravel

Introducción

Muchas veces necesitamos ciertas funciones o métodos, para nuestros proyectos, que por su repercusión o repetición, puede ser interesante tenerlos agrupados. Solemos llamarlos Helpers.

Podemos hacerlo de tres formas Crear un fichero de funciones (el más común entre los bloggers de Laravel) con carga mediante autload Creación de una Clase estática (no muy común pero más fina y menos propensa a choques) Creación de una paquete, que sería interesante si tuviéramos muchísimos helpers en distintas clases y que pudieran ser compartido por multitud de nuestros proyectos o de otros programadores Aquí vamos a mostrar los dos primeros.

helpers.php

Crearemos un fichero cuyo lugar y nombre podría ser app/helpers.php con el contenido de abajo.

Crear un archivo helpers.php

Método incorrecto

No se la de veces que lo habré visto, y me parece horrible y alejado de las buenas prácticas.

<?php 

function asString($data)
{
   $json = asJSON($data);

   return wordwrap($json, 76, "\n   ");
}


function asJSON($data)
{
   $json = json_encode($data);
   $json = preg_replace('/(["\]}])([,:])(["\[{])/', '$1$2 $3', $json);

   return $json;
}

Método correcto

Ya que no usamos una clase sino un archivo tipo include que cargaremos mediante un autload, para evitar problemas de duplicidad de nombres con las funciones de PHP, lo correcto es hacerlo como el código de abajo

<?php

if (!function_exists('asString'))
{
    function asString($data)
    {
        $json = asJSON($data);

        return wordwrap($json, 76, "\n   ");
    }
}

if (!function_exists('asJson'))
{
    function asJSON($data)
    {
        $json = json_encode($data);
        $json = preg_replace('/(["\]}])([,:])(["\[{])/', '$1$2 $3', $json);

        return $json;
    }
}

Carga del archivo

Editamos nuestro composer.json en la sección autoload añadiendo o creando la sección files

"autoload": {
     "psr-4": {
         "App\\": "app/",
         "Database\\Factories\\": "database/factories/",
         "Database\\Seeders\\": "database/seeders/"
     },
     "files": [
         "app/helpers.php
     ] 
},

Actualización del autoload de la app

composer dump-autoload

Uso

El uso es sencillo, ya que se le llama como si fuera una función nativa de PHP.

$headerData = [
   'category' => 'develop',
   'unique_args' => [
       'var_1' => 'abc'
   ]
];

$header = asString($headerData);

Crear una clase estática

La creación de una clase estática, nos permite más seguridad, algo más de estandarización y para trabajar en grupo, y el camino a la creación de nuestro propio paquete de Helpers.

Crear el fichero de clase Helper

En mi ejemplo uso el directorio Helpers dentro de App porque tengo más clases de helpers en un proyecto largo app/Helpers/MailHelpers

<?php

namespace App\Helpers;

class MailHelpers {
    public static function asString($data)
    {
        $json = self::asJson($data);

        return wordwrap($json, 76, "\n   ");
    }

    public static function asJson ($data)
    {
        $json = json_encode($data);
        $json = preg_replace('/(["\]}])([,:])(["\[{])/', '$1$2 $3', $json);

        return $json;
    }
}

De esta forma no necesitamos realizar ninguna modificación en nuestro fichero composer.json, ya que la carga se produce conforme al PSR-4, y es simplemente una clase más de tipo estático.

Uso

Este ejemplo es del uso de la clase MailHelpers en una clase Mail.

<?php

namespace App\Mail;

use App\Helpers\MailHelpers;
…

public function build()
{
  $headerData = [
      'category' => 'develop',
      'unique_args' => [
          'var_1' => 'abc'
      ]
  ];

  $header = MailHelpers::asString($headerData);
  ...
  ...

Aviso

Esta documentación y su contenido, no implica que funcione en tu caso o determinados casos. También implica que tienes conocimientos sobre lo que trata, y que en cualquier caso tienes copias de seguridad. El contenido el contenido se entrega, tal y como está, sin que ello implique ningún obligación ni responsabilidad por parte de Castris

Si necesitas soporte profesional puedes contratar con Castris soporte profesional.

Mailhog como mailtrap para desarrollos con Laravel

Introducción

Generalmente uso mailtrap.io para comprobar los correos ya que entre otras herramientas tiene HTML Check pero cuando se trata de trabajos en su inicio que no requieren de esto, y prima la portabilidad, prefiero usar Mailhog, un capturador de correo desarrollado en Go.

Instalación en Ubuntu

Descarga y conversión a ejecutable

Revisar siempre la versión disponible, ya que una entrada de blog o wiki puede no estar actualizada

$ wget https://github.com/mailhog/MailHog/releases/download/v1.0.0/MailHog_linux_amd64
$ sudo cp MailHog_linux_amd64 /usr/local/bin/mailhog
$ sudo chmod +x /usr/local/bin/mailhog

Crear un servicio para Mailhog (systemd)

Se hace uso del comando whoami para cargar el servicio para nuestro usuario. Debemos verificar que lo hizo bien.

$ sudo tee /etc/systemd/system/mailhog.service <<EOL
[Unit]
Description=Mailhog
After=network.target
[Service]
User=$(whoami)
ExecStart=/usr/bin/env /usr/local/bin/mailhog > /dev/null 2>&1 &
[Install]
WantedBy=multi-user.target
EOL

Verificar

$ sudo cat  /etc/systemd/system/mailhog.service
[Unit]
Description=Mailhog
After=network.target
[Service]
User=abkrim
ExecStart=/usr/bin/env /usr/local/bin/mailhog > /dev/null 2>&1 &
[Install]
WantedBy=multi-user.target

Activar y habilitar para arranque con el sistema

Es aconsejable siempre que modifiquemos algo en el systemd hacer un reload del demonio.

$ sudo systemctl daemon-reload

Distintas acciones

$ sudo systemctl start mailhog.service
$ sudo systemctl enable mailhog.service
$ sudo systemctl status mailhog.service

Configurar php.ini

En mi caso es para PHP-FPM y multi versión, por lo que necesito añadirlo a cada uno, para el fpm y para el cli.

$ sudo sed -i "s/;sendmail_path.*/sendmail_path='\/usr\/local\/bin\/mailhog sendmail abkrim@nox.test'/" /etc/php/8.0/fpm/php.ini
$ sudo sed -i "s/;sendmail_path.*/sendmail_path='\/usr\/local\/bin\/mailhog sendmail abkrim@nox.test'/" /etc/php/8.0/cli/php.ini
$ sudo sed -i "s/;sendmail_path.*/sendmail_path='\/usr\/local\/bin\/mailhog sendmail abkrim@nox.test'/" /etc/php/7.4/fpm/php.ini
$ sudo sed -i "s/;sendmail_path.*/sendmail_path='\/usr\/local\/bin\/mailhog sendmail abkrim@nox.test'/" /etc/php/7.4/cli/php.ini

Necesito un reload de php-fpm

$ sudo systemctl restart php8.0-fpm.service
$ sudo systemctl restart php7.4-fpm.service

Ver Mailhog en el navegador

http://localhost:8025/

Mailhog Localhost

Configuracion para Laravel

Editamos el fichero .env

El puerto por defecto es 1025 MAIL_FROM_ADDRESS es requerido

MAIL_MAILER=smtp
MAIL_HOST=localhost
MAIL_PORT=1025
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS=abkrim@nox.local
MAIL_FROM_NAME="${APP_NAME}"

Enlace

Aviso

Esta documentación y su contenido, no implica que funcione en tu caso o determinados casos. También implica que tienes conocimientos sobre lo que trata, y que en cualquier caso tienes copias de seguridad. El contenido el contenido se entrega, tal y como está, sin que ello implique ningún obligación ni responsabilidad por parte de Castris

Si necesitas soporte profesional puedes contratar con Castris soporte profesional.

Método en TestCase para facilitar los test de usuario logeado

Desarrollo

En los tests mucha sveces necesitamos realziar pruebas como usuario logeado o usuario especifico. Un buen refactor para esta acción repitida es incluirla en la clase TestCase de la cual extendemos test en Laravel.

TestCase

public function login(User $user = null): User
 {
     $user ??= User::factory()->create();

     $this->actingAs($user);

     return $user;
 }

Ahora será más fácil escribir nuestros tests usando simplemente $this->login

     //$this->actingAs(User::factory()->create());

     $this->login()

Aviso

Esta documentación y su contenido, no implica que funcione en tu caso o determinados casos. También implica que tienes conocimientos sobre lo que trata, y que en cualquier caso tienes copias de seguridad. El contenido el contenido se entrega, tal y como está, sin que ello implique ningún obligación ni responsabilidad por parte de Castris

Si necesitas soporte profesional puedes contratar con Castris soporte profesional.

Métodos dump en el proceso de Testing con Laravel

Introducción

Una de las herramientas que más me gustan de Laravel es dd(). Una herrmienta que permite un volocado con exit, que por lo general sale bien formateo, y que es muy útil en algunas corcustancias para localizar problemas o comprender mecanismo y estados en alguna parte del código.

En PHPUnit con Laravel tambien disponemos de herramientas para hacer algo parecido en el proceso de testing.

Volcando datos en la construcción de un test

Tenemos tres elementos todo ellos formando parte de la clase TestReponse de Iluminate/Response

dump()

Que vuelca el contenido de la respuesta (response)

dumpHeaders()

Que vuelca solo el contenido de las headers muy útil cuando trabajamos con Api auqne tambien útil en otras areas

dumpSession()

Que vuelca el contenido de la session de la respuesta

Ejemplo

/** @test */
function date_format_is_validate()
{
  $this->login();

  $post = BlogPost::factory()->create();

  $this
      ->post(action([BlogPostAdminController::class, 'update'], $post->slug), [
          'title' => $post->title,
          'author' => $post->author,
          'body' => $post->body,
          'date' => '01/01/2021',
      ])
      ->dumpSession()
      ->assertSessionHasErrors(['date']);
}

dumpSession en Tetstin Laravel

Aviso

Esta documentación y su contenido, no implica que funcione en tu caso o determinados casos. También implica que tienes conocimientos sobre lo que trata, y que en cualquier caso tienes copias de seguridad. El contenido el contenido se entrega, tal y como está, sin que ello implique ningún obligación ni responsabilidad por parte de Castris

Si necesitas soporte profesional puedes contratar con Castris soporte profesional.

Asignar múltiples variables a la vez en PHP

Introducción

La limpieza de código para su lectura es algo muy interesante. A veces tenemos métodos que devuelven un array numérico que queremos incorporar a una serie de variables. Podemos hacerlo al mismo tiempo.

[] = array()

En las pruebas de test, por ejemplo queremos evaluar dos usuario, uno con permisos y otro sin permisos.

Nuestra factoría (Laravel), nos permite obtener un array con dos colecciones en un array y podemos asignarlas a dos variables $guest y $admin

/** @test */
function only_admin_users_are_allowed()
{
  [$guest, $admin] = User::factory()
      ->count(2)
      ->sequence(
          ['is_admin' => false],
          ['is_admin' => true],
      )
      ->create();

Similar trabajo tiene la función list() aunque en este caso no me parece tan limpia.

Aviso

Esta documentación y su contenido, no implica que funcione en tu caso o determinados casos. También implica que tienes conocimientos sobre lo que trata, y que en cualquier caso tienes copias de seguridad. El contenido el contenido se entrega, tal y como está, sin que ello implique ningún obligación ni responsabilidad por parte de Castris

Si necesitas soporte profesional puedes contratar con Castris soporte profesional.

Configurar Carbon::now() a una fecha para trabajar con tests

Código de estado HTTP para llamadas API

Códigos de respuesta HTTP

Los códigos de estado de respuesta HTTP indican si se ha completado satisfactoriamente una solicitud HTTP específica. Las respuestas se agrupan en cinco clases:

  1. Respuestas informativas (100–199),
  2. Respuestas satisfactorias (200–299),
  3. Redirecciones (300–399),
  4. Errores de los clientes (400–499),
  5. Errores de los servidores (500–599).

Los códigos de estado se definen en la sección 10 de RFC 2616. Puedes obtener las especificaciones actualizadas en RFC 7231.

Tabla de uso más cotidiano

No estan todos, pero si los que uso yo, que muchas veces no lo hago por que me quede claro, sino porque Laravel lo hace así, y pese a que en algunos casos no estoy de acuerdo, creo que Taylor sabe más.

Código Respuesta Apreciaciones
100 Continue
200 Ok No todo es 200 y es una manía extendida entre programadores no actualizados
201 Created Típica respuesta de un PUT con resultado correcto
202 Accepted Solicitud sin compromiso, es decir no hay respuesta asincrona
301 Moved Permanently La URI se modifico
400 Bad request Posiblemente una mala sintaxis en la llamada a la api
401 Unauthorized Es necesiario autenticarse. Similar a 403 pero indicando que si se puede logear haciendolo debdamente
403 Forbidden El login no es valido para acceder al recurso solicitado
404 Not found El servidor no pudo encontrar el contenido solicitado. El más famoso
422 Unprocessable Entity La petición estaba bien formada pero no se pudo seguir debido a errores de semántica. Usado por laravel para muchas cosas.
429 Too manya requests Exceso de peticiones en un periodo de tiempo. (Throttling)
500 Internal Server Error El servidor ha encontrado una situación que no sabe cómo manejarla
502 Bad Gateway El servidor anda raro
503 Service unavailable El servidor no está listo para manejar la petición. Causas comunes puede ser que el servidor está caído por mantenimiento o está sobrecargado.
Fuente

Códigos de estado de respuesta HTTP

Aviso

Esta documentación y su contenido, no implica que funcione en tu caso o determinados casos. También implica que tienes conocimientos sobre lo que trata, y que en cualquier caso tienes copias de seguridad. El contenido el contenido se entrega, tal y como está, sin que ello implique ningún obligación ni responsabilidad por parte de Castris

Si necesitas soporte profesional puedes contratar con Castris soporte profesional.

Testing error SQLSTATE[HY000]: General error: 1 near "ALTER": syntax error (SQL: ALTER TABLE

Introducción

Algunas veces hay que modificar columnas en nuestros desarrollos. Laravel nospermite la creación de migraciones especializadas en este tipo de acciones, pero supeditadas a Doctrine/dbal el cual muchas cosas no las hace

Presta atención a esa peculiaridad sobre todo en el uso de cosas como las columnas enum y cosas parecidas que son quebraderos de cabeza a demás de poco efectivas en su uso.

Me leve una sopresa cuando quise cambiar un unsignedTinyInteger.


public function up()
{
    // Not work because doctirne not work with tinyInteger and others
    Schema::table('subscribers', function (Blueprint $table) {
        $table
            ->tinyInteger('status')->unsigned()
            ->default(array_search('Pendiente', Subscriber::STATUS_SELECT))
            ->change();
    });
}

Error

> a migrate
Migrating: 2022_02_02_124904_modidy_status_default_value_to_subscribers_table

   Doctrine\DBAL\Exception 

  Unknown column type "tinyinteger" requested. Any Doctrine type that you use has to be registered with \Doctrine\DBAL\Types\Type::addType(). You can get a list of all the known types with \Doctrine\DBAL\Types\Type::getTypesMap(). If this error occurs during database introspection then you might have forgotten to register all database types for a Doctrine Type. Use AbstractPlatform#registerDoctrineTypeMapping() or have your custom types implement Type#getMappedDatabaseTypes(). If the type name is empty you might have a problem with the cache or forgot some mapping information.

Solución

El uso de raw, pero teniendo una atención relativa al entorno de testing, ya que si no lo hacemos asi, cuandpo ejecutemos nuestras pruebas (test) obtenderiamos un error.

There was 1 error:

1) Tests\Http\Controllers\Api\Admin\Subscriber\SubscriberControllerStoreWithCampaignTest::call_with_correct_params_create_new_subscriber_associate_a_one_campaign
Illuminate\Database\QueryException: SQLSTATE[HY000]: General error: 1 near "ALTER": syntax error (SQL: ALTER TABLE mailer.subscribers ALTER status SET DEFAULT 1)

Para ello editaremos la migración de nuestra tabla

public function up()
{
    if (App::environment() !== 'testing') {
        DB::statement(
          'ALTER TABLE mailer.subscribers ALTER status SET DEFAULT '
          . array_search('Pendiente', Subscriber::STATUS_SELECT)
        );
    }
}

Editado 09/02/2022

Tras actualizar en producción me di cuenta de un error. Estoy definiendo en el código el nombre exacto de la base de datos, y no es correcto.

SQLSTATE[42000]: Syntax error or access violation: 1142 ALTER command denied to user 'cwcl_user'@'localhost' for table 'subscribers' (SQL: ALTER TABLE mailer.subscribers ALTER status SET DEFAULT 1)

El código de abajo lo arregla.

public function up()
{
    if (App::environment() !== 'testing') {
        DB::statement(
          'ALTER TABLE '
          . config('database.connections.mysql.database')
          . '.subscribers ALTER status SET DEFAULT '
          . array_search('Pendiente', Subscriber::STATUS_SELECT)
        );
    }
}

Esto tambien nos obliga a aditar la migración inicial con el valor adecuado ya que de lo contrario, en nuestros tests, no tendríamos el valor por defecto deseados para esa columna.

De esta forma, podemos seguri trabajando tanto en local como en remoto, si tenemos que actualziar alli.

Otras opciones

Seguro que puede haber otras opciones. Pero yo use esta y me funciono. Si tienes otra, no dudes en conatctact conmigo. abdelkarim.mateos[laarroba]castris.com

Aviso

Esta documentación y su contenido, no implica que funcione en tu caso o determinados casos. También implica que tienes conocimientos sobre lo que trata, y que en cualquier caso tienes copias de seguridad. El contenido el contenido se entrega, tal y como está, sin que ello implique ningún obligación ni responsabilidad por parte de Castris

Si necesitas soporte profesional puedes contratar con Castris soporte profesional.

Laravel rescue() helper

rescue()

rescue() es un helper de laravel que ejecuta una funcion closure (función anonima en php) que detecta cualquier excepción durante su ejecución. Las excepciones se enviarán a su controlador de excpeciones, pero la solicitud continuara siendo procesada.

// Viejo método - try / catch ignorando la excepción. Un poco feo
private static function existsOnCDN(string $path): bool
{
  $cdn = $false;
  
  try {
    $cdn = Storage::disk('cdn')->exists($path);
  } catch (\Exception $e) {
    // CDN no esta disponible por problemas de red.
  }
  return $cdn;
}

// Mas claro, rescue() ignora la excepcion y permite al código continuar. Opcionalmente podemos pasar un valor de retorno
private static function existsOnCDN(string $path): bool
{
  return rescue(fn () => Storage::disk('cdn')->exists($path), false);	
}

Agradecimientos

A @shawnlindstrom por su tuit

Aviso

Esta documentación y su contenido, no implica que funcione en tu caso o determinados casos. También implica que tienes conocimientos sobre lo que trata, y que en cualquier caso tienes copias de seguridad. El contenido el contenido se entrega, tal y como esta, sin que ello implique ningún obligación ni responsabilidad por parte de Castris

Si necesitas soporte profesional puedes contratar con Castris soporte profesional.

Sail, Access can't connect to Mysql

Introducción

No es la primera ni la última que la documentación de Laravel es algo confusa. En este caso, siguiendo escrupulosamente las instrucciones de Laravel Sail pero al configurar mi TablePlus me da error de conexión.

Solución

En primer lugar añadir al fichero .env de nuestro proyecto,

FORWARD_DB_PORT=3306

Apagar si está encendido, sail.

Ejecutar en nuestra máquina

> sail up -d
sail up -d
example-app-laravel.test-1   "start-container"   laravel.test        exited (0)
Shutting down old Sail processes...
[+] Running 5/5
 ⠿ Network example-app_sail              Created                                                                                                                  0.0s
 ⠿ Container example-app-mysql-1         Started                                                                                                                  0.6s
 ⠿ Container example-app-redis-1         Started                                                                                                                  0.4s
 ⠿ Container example-app-mailhog-1       Started                                                                                                                  0.6s
 ⠿ Container example-app-laravel.test-1  Started                                                                                                                  0.9s

> sail artisan config:cache
   INFO  Configuration cached successfully.
> sail artisan migrate

   INFO  Preparing database.

  Creating migration table ............................................................................................................... 29ms DONE

   INFO  Running migrations.

  2014_10_12_000000_create_users_table ................................................................................................... 40ms DONE
  2014_10_12_100000_create_password_resets_table ......................................................................................... 26ms DONE
  2019_08_19_000000_create_failed_jobs_table ............................................................................................. 27ms DONE
  2019_12_14_000001_create_personal_access_tokens_table .................................................................................. 41ms DONE

Y ahora sí, que podremos conectarnos.

Table Plus

Aviso

Esta documentación y su contenido, no implica que funcione en tu caso o determinados casos. También implica que tienes conocimientos sobre lo que trata, y que en cualquier caso tienes copias de seguridad. El contenido el contenido se entrega, tal y como está, sin que ello implique ningún obligación ni responsabilidad por parte de Castris

Si necesitas soporte profesional puedes contratar con Castris soporte profesional.