Espera un momento ...
¿Te gustaría llevar mi curso de Laravel, gratis?
Sólo debes ingresar tus datos:
¿Por qué usar Service Providers?
Laravel es un framework que nos facilita el desarrollo de aplicaciones web.
Usar Laravel es muy sencillo.
Sin embargo, si queremos entender realmente cómo funciona o cuál es la filosofía que sigue, es importante comprender 2 conceptos (service container y service provider).
Laravel como framework usa un "contenedor de servicios" y unos "proveedores de servicios" siempre que necesita iniciar una instancia de una aplicación Laravel.
No solo nuestras aplicaciones, sino también los servicios que conforman el núcleo de Laravel, se inician gracias a los service providers
.
service providers
se encargan de toda la configuración necesaria antes de empezar a usar un servicio.
Dependiendo del servicio que se va a iniciar, el proveedor se encarga de crear nuevas instancias y posiblemente las relaciones con otros servicios (definir parámetros, oyentes de eventos, middlewares, rutas).La creación de nuevos paquetes para el framework Laravel requiere conocer bien estos conceptos.
¿Cómo usar Service Providers?
Más adelante, en este artículo, vamos a ver cómo crear nuestro propio service provider.
De momento, un breve adelanto:
config/app.php
presente en los proyectos Laravel, contiene un arreglo llamado providers
.¿Cuándo usar Service Providers?
Cuando desarrollamos una aplicación muy puntual, generalmente tenemos secciones bastante simples, donde basta con validar y registrar datos.
En estos casos no es necesario usar Service Providers
.
Sin embargo, hay aplicaciones que van más allá de registrar datos. Por ejemplo:
En estos casos, tenemos muchas formas de hacer nuestra implementación.
Entonces, dependiendo de la magnitud del problema a resolver, hemos de optar por "crear un servicio que resuelva fácilmente nuestras necesidades".
service provider
(un proveedor, para que se encargue de la configuración de este servicio, y nosotros simplemente lo usemos).En este artículo:
service container
de Laravel.A modo de ejemplo, el servicio que nos interesa, va a estar relacionado con lo siguiente:
(Hipotéticamente) ya he desarrollado la funcionalidad para subir archivos CV en lote.
Lo bueno es que ya funciona para la subida en lote. Pero, repetir código, a fin de tener la misma funcionalidad en distintas secciones de la aplicación, no es adecuado.
Si estás siguiendo este artículo y ya has pensado en cómo aplicar estos conceptos. Mi sugerencia es la siguiente:
No es imposible implementar nuevas características mientras se refactoriza. Pero no es recomendable.
Laravel, como siempre, nos facilita las cosas:
php artisan make:provider CvUploaderServiceProvider
CvUploaderServiceProvider.php
con la estructura básica que un service provider
debe tener.app/Providers
.Si abres el archivo te encontrarás con una clase del mismo nombre, que contiene 2 métodos: register
y boot
.
En breve vamos a ver la diferencia entre estos 2 métodos. Pero antes debemos registrar nuestro proveedor ante Laravel.
Para ello vamos al archivo config/app.php
y dentro del arreglo providers
añadimos el que acabamos de crear:
'providers' => [
/*
* Laravel Framework Service Providers...
*/
Illuminate\Auth\AuthServiceProvider::class,
Illuminate\Broadcasting\BroadcastServiceProvider::class,
// [...]
Illuminate\Validation\ValidationServiceProvider::class,
Illuminate\View\ViewServiceProvider::class,
/*
* Package Service Providers...
*/
Laravel\Tinker\TinkerServiceProvider::class,
/*
* Application Service Providers...
*/
App\Providers\AppServiceProvider::class,
App\Providers\AuthServiceProvider::class,
// App\Providers\BroadcastServiceProvider::class,
App\Providers\EventServiceProvider::class,
App\Providers\RouteServiceProvider::class,
App\Providers\CvUploaderServiceProvider::class,
],
Como su mismo nombre lo indica, nos permite registrar. ¿Pero registrar qué y sobre qué?
service container
.¿Qué representan estos bindings? ¿Qué enlazan?
Registrar un binding en el service container
de Laravel es decirle al contenedor cómo instanciar un objeto en particular.
El service container
de Laravel nos permite registrar distintos tipos de bindings.
Recapitulando, respecto al método register
: es un método que le permite a nuestro ServiceProvider registrar bindings en el contenedor de Laravel.
Esto es posible definiendo un binding en el método register de nuestro service provider.
Pero para esto necesitamos: haber definido la clase que queremos instanciar (una clase que represente a nuestro servicio).
En este caso voy a crear una clase llamada CvHandler
. Recuerda que puedes crear esta clase donde mejor te parezca.
La clase que he creado está disponible bajo el namespace Tawa\Services
(siendo Tawa el nombre de la aplicación que estoy desarrollando, y Services la carpeta que contiene todos los servicios creados para esta aplicación).
Entonces hemos de registrar el binding de la siguiente manera (dentro del método register):
$this->app->bind(CvHandler::class, function ($app) {
return new CvHandler();
});
Esta es una de las tantas formas de registrar un binding.
¿Por qué es necesario un closure? (la función que aparece como segundo parámetro).
Buena observación. En este caso no es necesario. Pero cuando una clase tiene dependencia respecto a otras, es justamente aquí donde se crea la instancia con la configuración requerida; y es éste uno de los mayores beneficios de usar un service provider.
Veamos un ejemplo, de una clase que requiere de ciertos parámetros para su correcto funcionamiento en nuestra aplicación.
$unaInstancia = new ClaseA(new ClaseB(config('secret_key')), new ClaseC(new ClaseX(), new ClaseY()), new ClaseD('pym', 7));
En este ejemplo:
Usar este código en un controlador y/o en todas las clases que necesitemos una instancia de ClaseA, no es bueno.
Es aquí donde un proveedor de servicios nos proveería del servicio que brinda ClaseA.
Para ello habría que registrar un binding del siguiente modo:
$this->app->singleton(ClaseA::class, function ($app) {
return new ClaseA(new ClaseB(config('secret_key')), new ClaseC(new ClaseX(), new ClaseY()), new ClaseD('pym', 7));
});
Si eres observador, habrás notado que en este caso hemos usado singleton
en vez de bind
. Es otra forma de crear un binding.
En este caso haciendo uso del patrón de diseño Singleton.
En vez de llamar al closure cada vez que se necesite una instancia, tendríamos una misma instancia compartida en todo nuestro proyecto.
Volviendo a nuestro ejemplo inicial.
Si nuestro servicio no requiere de ninguna configuración, y tampoco se asocia con ninguna interfaz, entonces no es necesario registrarlo en el service container (la inyección de dependencias funcionará con esta clase incluso sin darle instrucciones a Laravel).
Sin embargo, es recomendable asociar una interfaz a nuestros servicios. De esta forma, será posible usar otra clase (con una implementación distinta) bajo otras circunstancias.
Por ejemplo, podemos usar una implementación distinta para nuestro entorno local y nuestro entorno de producción. También podemos usar una implementación distinta para la ejecución de pruebas automatizadas.
Como ya comenté antes, si nuestra clase no tiene dependencias y no implementa ninguna interfaz, no es necesario crear un binding.
Su uso estaría disponible sin ningún paso adicional:
class UploadController extends Controller
{
public function store(CvHandler $cvHandler)
{
// aquí es posible usar $cvHandler para procesar las hojas de vida
}
}
Pero, si queremos asociar una interfaz a nuestra clase (que es lo más recomendable), debemos registrar un binding en el método register:
$this->app->bind(CvHandlerInterface::class, CvHandler::class);
De esta forma podemos decir que:
Un service provider
ha registrado nuestro servicio en el service container
, y podemos usar nuestro servicio desde donde nos plazca (gracias a la inyección de dependencias).
Y si usamos una interfaz, el uso sería del siguiente modo:
public function store(CvHandlerInterface $cvHandler)
{
// aquí es posible usar $cvHandler para procesar las hojas de vida
}
Sin importar cómo se resuelva esta inyección de dependencias, $cvHandler
debe ser capaz de usar los métodos que dicta la interfaz.
Para el ejemplo que estábamos viendo, como mínimo la interfaz exigiría el siguiente método:
<?php namespace Tawa\Interfaces;
use App\User;
interface CvHandlerInterface
{
public function uploadCV($uploader, $owner, UploadedFile $file);
}
Y todas nuestras implementaciones estarían en la obligación de definir este método:
class CvHandler implements CvHandlerInterface
{
public function uploadCV($uploader, $owner, UploadedFile $cv)
{
// TODO: Implementar el método uploadCV()
}
}
Este ejemplo no tiene relación directa con la comprensión de los conceptos antes mencionados. Sin embargo es interesante ver cómo cambia la organización del código.
El siguiente método store
se usa para subir un CV a S3 y guardar su contenido en la base de datos.
Antes.
public function store(Request $request)
{
$rules = [
'cv' => 'required|mimes:pdf,doc,docx|max:10000'
];
$this->validate($request, $rules);
$cv = $request->file('cv');
$extension = $cv->getClientOriginalExtension();
$uniqueId = uniqid(); // current time considering microseconds
$fullPath = "cv/anonymous/$uniqueId.$extension";
$fileContents = file_get_contents($cv);
Storage::disk('s3')->put($fullPath, $fileContents);
$successfulUpload = Storage::disk('s3')->exists($fullPath);
$saved = false;
if ($successfulUpload) {
// parse the CV document
if ($extension == 'pdf') {
$parser = new Parser();
$pdf = $parser->parseFile($cv);
$text = $pdf->getText();
} else { // doc, docx
$filename = $cv->path();
$mimeType = File::mimeType($cv);
$text = DocumentParser::parseFromFile($filename, $mimeType);
}
// store the resume in the db
$resume = new Resume();
$resume->owner_id = null; // anonymous cv owner
$resume->uploader_id = auth()->user()->id;
$resume->file_name = "$uniqueId.$extension"; // with extension
$resume->content = $text;
$saved = $resume->save();
}
if ($saved)
return response()->json('success', 200);
// else
return response()->json('error', 500);
}
Aquí tenemos muchas cosas en juego:
file
de la clase Request
de Laravel, para obtener un objeto con información del archivo subido.pdf
se usa una instancia de PdfParser\Parser
para obtener el texto del archivo.pdf
se asume que es .doc
o .docx
, y se usa la clase DocumentParser
(es una clase que he definido con métodos estáticos, pero vamos a cambiar esto, porque se debe evitar el uso de métodos estáticos siempre que sea posible, por diversas razones).Luego de mover la lógica a CvHandler
el método queda de la siguiente manera:
Después.
public function store(Request $request, CvHandlerInterface $cvHandler)
{
$rules = [
'cv' => 'required|mimes:pdf,doc,docx|max:10000'
];
$this->validate($request, $rules);
$cv = $request->file('cv');
$saved = $cvHandler->uploadCV(auth()->id(), null, $cv);
if ($saved)
return response()->json('success', 200);
// else
return response()->json('error', 500);
}
El método uploadCV se está llamando con un 2do parámetro que es null. Esto es así porque el CV no se asocia con ningún usuario.
En la subida de archivos en lote, un administrador sube CVs sin asociarlos a usuarios postulantes.
Siguiendo esta idea, CvHandler
quedaría del siguiente modo y podría usarse desde distintos lugares de la aplicación:
class CvHandler implements CvHandlerInterface
{
protected $storage;
protected $pdfParser;
protected $docParser;
public function __construct(Storage $storage, Parser $pdfParser, DocumentParser $docParser)
{
$this->storage = $storage;
$this->pdfParser = $pdfParser;
$this->docParser = $docParser;
}
private function getUniqueFileName(UploadedFile $cv)
{
$uniqueId = uniqid();
$extension = $cv->getClientOriginalExtension();
return "$uniqueId.$extension";
}
private function uploadFile($fullPath, UploadedFile $cv)
{
$fileContents = file_get_contents($cv);
$this->storage->disk('s3')->put($fullPath, $fileContents);
return $this->storage->disk('s3')->exists($fullPath);
}
private function getTextFromDocument(UploadedFile $cv)
{
$extension = $cv->getClientOriginalExtension();
if ($extension == 'pdf') {
$pdf = $this->pdfParser->parseFile($cv);
return $pdf->getText();
} else { // doc, docx
$filename = $cv->path();
try {
return $this->docParser->parseFromFile($filename);
} catch (Exception $e) {
return null;
}
}
}
public function uploadCV($uploader, $owner, UploadedFile $cv)
{
$fileName = $this->getUniqueFileName($cv);
if ($owner)
$fullPath = "cv/$owner/$fileName";
else
$fullPath = "cv/anonymous/$fileName";
$successfulUpload = $this->uploadFile($fullPath, $cv);
$saved = false;
if ($successfulUpload) {
$text = $this->getTextFromDocument($cv);
if ($text) {
// store the resume text in the db
$resume = new Resume();
$resume->owner_id = $owner; // anonymous cv owner or user applicant id
$resume->uploader_id = $uploader; // admin id or user applicant id
$resume->file_name = $fileName; // contains the extension
$resume->content = $text;
$saved = $resume->save();
}
}
return $saved;
}
}
Perdón si lo siguiente suena redundante, pero lo diré de todas formas, a modo de resumen.
service provider
es un proveedor de servicios, y como tal se encarga de registrar nuestros servicios ante el service container
.service provider
porque la inyección de dependencias funcionará de todas formas (Laravel lo resuelve usando reflection).Si quieres aprender más, te invito a seguir mis cursos (los encuentras aquí debajo) ?.
Así mismo, si tienes dudas o sugerencias, recuerda que todo comentario es bienvenido.
Comparte este post si te fue de ayuda 🙂.
Regístrate
Accede a todos los cursos, y resuelve todas tus dudas.
Cursos Recomendados
Aprende Laravel desde cero y desarrolla aplicaciones web reales, en tiempo récord, de la mano de Laravel.
Iniciar cursoActualiza tus proyectos desde cualquier versión hasta la última versión estable de Laravel.
Iniciar cursoDesarrollemos un Messenger! Aprende sobre Channels, Queues, Vuex, JWT, Sesiones, BootstrapVue y mucho más.
Iniciar cursoEspera un momento ...
¿Te gustaría llevar mi curso de Laravel, gratis?
Sólo debes ingresar tus datos: