Agregar comando Artisan de Laravel para backups de la base de datos

El objetivo del siguiente contenido es explicar como extender la funcionalidad de artisan agregando un nuevo comando que permita generar periódicamente un respaldo de la base de datos, para esto ultimo haremos uso de las tareas programadas con crontab.

linux+laravel+crontab

linux+laravel+crontab

Consideraciones

Vamos a considerar un par de cosas:

  1. Nuestro sistema se encuentra en un servidor Linux
  2. La base de datos es MySQL
  3. Usaremos Laravel 5 o superior

Artisan

Artisan es la herramienta de lineas de comandos de Laravel la cual nos permite hacer distintas tareas por ejemplo mapear la base de datos, crear modelos, vistas, migrates, limpiar cache, etc..

Para saber mas podemos listar las opciones disponibles, esto lo podemos hacer con la siguiente instrucción:

php artisan list

Agregando Nuevo comando

Lo siguiente seria agregar el nuevo comando db:dump para esto vamos a escribir la siguiente instrucción:

php artisan make:command DbDump

Esto nos va a generar la plantilla de un comando, en el archivo app/Console/Commands/DbDump.php al cual solo le faltaría agregar nuestro código, vamos a empezar re-definir las propiedades:

protected $signature = 'db:dump';

Esta propiedad($signature) define la forma como se debe invocar el comando para ser ejecutado, en este caso estamos diciendo que se mande a llamar como php artisan db:dump.

La siguiente propiedad a modificar es la descripción($description), la cual la definiremos como:

protected $description = 'Respalda la base de datos, comprimiendo los datos en un archivo .gz';

Finalmente vamos a poner un mensaje en la función handle la cual se ejecuta cuando se llama el comando para la cual quedaría como:

public function handle() {
   $this->line('<fg=red;bg=yellow>Soy el nuevo comando db:dump</>');
}

Declarar el comando

Lo siguiente seria declarar el comando nuevo para que este disponible y la próxima vez listemos los comandos de Artisan aparezca, para esto abrimos el archivo app/Console/Kernel.php y modificamos el valor del atributo $commands quedando de la siguiente manera:

protected $commands = [
   \App\Console\Commands\DbDump::class
 ];

Ahora podemos probar que el comando este disponible ejecutando: php artisan list y donde nos debería de aparecer el nuevo comando, el cual para ejecutarlo tendríamos que escribir:

php artisan db:dump

Codificando la función handle

Finalmente vamos a codificar la función handle, la cual dejó aquí:

public function handle() {
	$ds = DIRECTORY_SEPARATOR;
	$host = env('DB_HOST');
	$username = env('DB_USERNAME');
	$password = env('DB_PASSWORD');
	$database = env('DB_DATABASE');
	$path = database_path('backups' . $ds);
	$file = 'bd' . date('_Y-m-d') . '.sql';
	if (!is_dir($path)) {
			mkdir($path, 0755, true);
	}
	$this->line('<fg=cyan>Backup: </><fg=yellow;bg=black>'. $path . $file . '</>');
	# Generamos el comando con mysqldump para exportar los datos
	$command = sprintf(
		'mysqldump --skip-comments --skip-compact --no-create-info'
		. ' --skip-triggers --complete-insert --skip-add-locks'
		. ' --disable-keys --lock-tables --host="%s" --user="%s" '
		, $host, $username
		);
	if (!empty($password)) {
		$command .= sprintf('--password="%s" ', $password);
	}
	$command .= sprintf('%s > "%s"', $database, $path . $file);
	$this->line('<fg=green>CMD: </><fg=yellow;bg=black>'. $command . '</>');
	exec($command, $output, $return);
	if ($return) {
		$this->line('<fg=red;bg=yellow>Error al intentar generar el Backup</>');
		if (file_exists($path . $ds . $file)) {
			unlink($path . $ds . $file);
		}
		return; // error
	}
	// Comprimiendo el archivo:
	// mayor info: https://www.php.net/manual/es/function.gzopen.php
	// Open the gz file (w9 is the highest compression)
	$fileCompress = gzopen ($path . $ds . $file . '.gz', 'w9');
	// Compress the file
	gzwrite ($fileCompress, file_get_contents($path . $ds . $file));
	// Close the gz file and we are done
	gzclose($fileCompress);
	// Generando el esquema
	$path = database_path('backups' . $ds . 'schemas'. $ds);
	$file = 'schema.sql';
	if (!is_dir($path)) {
		mkdir($path, 0755, true);
	}
	# Generamos el comando con mysqldump para exportar la estructura
	$command = sprintf(
		'mysqldump --skip-comments --skip-compact '
		. ' --no-data --host="%s" --user="%s" '
		, $host, $username
		);
	if (!empty($password)) {
		$command .= sprintf('--password="%s" ', $password);
	}
	$command .= sprintf(
		'%s | sed "s/ AUTO_INCREMENT=[0-9]*//g"  > "%s"',
		$database, $path . $file
	);
	$this->line('<fg=magenta>Generando Schema</>');
	exec($command, $output, $return);
	if ($return) {
		$this->line('<fg=red;bg=yellow>Error al intentar generar el Schema</>');
		if (file_exists($path . $ds . $file)) {
			unlink($path . $ds . $file);
		}
		return; // error
	}
}

Declarar la tarea en el crontab

Crontab es un servicio que nos permite agregar tareas que serán ejecutadas periódicamente, lo siguiente que nos faltaría seria declarar una tarea, para acceder al modo edición de reglas tenemos que ejecutar crontab -e la sintaxis de como declarar una regla se describe a continuación:

.--------------- minuto (0-59) 
|  .------------ hora (0-23)
|  |  .--------- día del mes (1-31)
|  |  |  .------ mes (1-12) o jan,feb,mar,apr,may,jun,jul... (meses en inglés)
|  |  |  |  .--- día de la semana (0-6) (domingo=0 ó 7) o
|  |  |  |  |     sun,mon,tue,wed,thu,fri,sat (días en inglés) 
|  |  |  |  |
*  *  *  *  *  comando a ejecutar

En este caso lo pondré a que se ejecute todos los días a las 2am, esto seria con la siguiente declaración de regla:

#.-------------- minuto cero (0-60)
#| .------------ hora dos (0-23)
#| |  .--------- todos los días (1-31)
#| |  |  .------ todos los meses (1-12)
#| |  |  |  .--- Cualquier dia de la semana (0-6)
#| |  |  |  |
#| |  |  |  |
0  2  *  *  *  php ruta_completa_larvel/artisan db:dump

Ejemplo completo

Dejo aquí el código completo del archivo, el cual lo puse en mi github:

https://gist.github.com/fitorec/d7f8003ff1b5501177068f00087cb51a

Enlaces recomendados

Por último les dejó un par de enlaces en donde puedan abundar un poquito mas al respecto:

Cargando distintos archivos .Env para cada subdominio con Laravel

En este post quisiera explicar acerca de una implementación que permita cargar distintas configuraciones para cada subdominio, el objetivo es tener una aplicación desarrollada con Laravel(5.8) que a partir de cada subdominio cargue una configuración distinta, de esta forma por ejemplo para cliente1.miapp.com se conectara a su propia base de datos diferente a clientex.miapp.com, la siguiente imagen trata de ilustrar este comportamiento.

Cargando configuración por subdominio con Laravel

Cargando configuración por subdominio con Laravel

Ciclo de Vida de Laravel

Quisiera detenerme un momento para explicar a grandes rasgos como funciona el ciclo de vida de Laravel, cuando accedemos a una aplicación de Laravel lo primero que cargamos es el archivo /public/index.php este a su vez manda a cargar dos archivos /vendor/autoload.php y /bootstrap/app.php el primero es generado por composer y se encarga de cargar todas las dependencias de terceros, este archivo no lo debería modificar por que en cada actualización se perdería, el segundo este es el indicado.

Si deseas profundizar en el tema te recomiendo la siguiente lectura: https://laravel.com/docs/5.8/lifecycle#lifecycle-overview, si te gusta explorar el código fuente de Laravel puedes ver el archivo en el archivo /public/index.php en github, en al siguiente imagen trataré de ejemplificar lo que ocurre cuando se ingresa en una aplicación de Laravel realizando un request desde nuestro navegador.

Ciclo de vida de Laravel

Ciclo de vida de Laravel

Codificando la solución

Como se comento los cambios los realizaremos en el archivo /bootstrap/app.php este archivo genera una instancia de Illuminate\Foundation\Application como podemos ver en la linea 14 del código original, ahora lo que vamos hacer es que a partir de la linea 17 vamos agregar el código encargado de detectar el subdominio (o ver si estamos en modo CLI):

$app = new Illuminate\Foundation\Application(
    $_ENV['APP_BASE_PATH'] ?? dirname(__DIR__)
);
/*
|--------------------------------------------------------------------------
| Cargando configuración a partir de el subdominio (SUBDOMAIN)
|--------------------------------------------------------------------------
*/
if (!defined('SUBDOMAIN')) {
	$subdomain_tmp = 'localhost';
	if (isset($_SERVER['SERVER_NAME'])) {
		$domainParts = explode('.', $_SERVER['SERVER_NAME']);
		$subdomain_tmp =  array_shift($domainParts);
	} else {
		if (php_sapi_name() === 'cli') {
			//Modo CLI útil a futuro
		}
	}
	define('SUBDOMAIN', $subdomain_tmp);
}
$app->loadEnvironmentFrom('subdomains_config' . DIRECTORY_SEPARATOR . '.' . SUBDOMAIN);

 

Ahora solo faltaría agregar en la carpeta subdomains_config archivos de configuración con el nombre de cada subominio precedido con un punto(.) por ejemplo para el clientex.app.com. el subdominio es clientex. y el archivo de configuración seria subdomains_config/.clientex.

Cambiando el sistema de cache

Como queremos que cada subdominio tengan su propio cache vamos a cambiar la ruta en donde guardan el cache de tal forma que genere una carpeta de cache para cada subdominio, para esto modificamos el archivo /config/cache.php la linea 52 la cambiamos por:

'path' => storage_path('framework/cache/data_' . SUBDOMAIN),

¿Que sigue?

Con esto ya quedaría una aplicación que cargue determinada configuración a partir de un subdominio, faltaría agregar comandos de artisan que permitan realizar tareas administrativas en todos los subdominios como por ejemplo respaldar las bases de datos, tareas que se puedan programar en nuestro servidor utilizando crontab, en el siguiente posts espero escribir al respecto.

¿Dudas? o comentarios

Espero el contenido haya quedado bien explicado si existieran dudas, comentario o aportaciones son bienvenidos :¬D sin mas me despido y felices códigos.

PS

Recomiendo leer los siguientes posts:

 

Alias GIT para visualizar las bitácoras

Dejo aquí un simple snippet que permite crear el alias git lg el cual nos visualiza las bitácoras de las ramificaciones de forma útil:

git config --global alias.lg \
"log --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset  %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit --date=relative"

Modo de uso

git lg

Borrando el alias

Si no te gusta el alias puedes eliminarlo de la manera siguiente:

git config --global --unset alias.lg

Otro Alias ADOG

Otro alias interesante que me encontré por la red que es el adog o bien all —decorate —oneline —graph:

git config --global alias.adog \
 "log --all --decorate --oneline --graph"

¿Cual otro snippet tienes para mostrar las bitácoras?

Deja tu comentario =)

Tesla – AC/DC

El 10 de julio se “celebro” el natalicio de Nicola Tesla.

IMG-20160908-WA0003

Esta imagen emblema su lucha mas significativa(vs Edison).

Actualmente la compañía Tesla Motors tiene fuerte rivalidad VS Apple Computers, de la cual yo le apuesto a Tesla Motors ya que desde mi punto de vista es la que realmente se esta enfocando en desarrollar tecnologías nuevas en estos Autos que se conducen solos.

Pero tú ¿que opinas?.

Humor – pro Java

IMG-20170331-WA0001

Un poco de humor pro-java, en ocasiones defendemos a capa y espera determinado lenguaje, en lo personal considero que: el lenguaje no hace al programador(su calidad) pero si afecta directamente el producto final(el software), sin embargo también considero que los lenguajes cada vez van tendiendo a ser mas ergonómicos respecto la mente humana, no hace falta recordar la opinión que tenia Dijkstra respecto a la estructura GOTO.