Creando nuestro service provider
Laravel, como siempre, nos facilita las cosas:
php artisan make:provider CvUploaderServiceProvider
- Este comando creará un archivo
CvUploaderServiceProvider.php
con la estructura básica que unservice provider
debe tener. - El archivo se creará en la carpeta
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,
],
El método register
Como su mismo nombre lo indica, nos permite registrar. ¿Pero registrar qué y sobre qué?
- Nos permite registrar «bindings» (esto es, terminología que usa Laravel en su documentación; en español sería enlaces).
- Y estos bindings se registran sobre el
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 uso más básico es instanciar un objeto de una clase que no tiene ninguna dependencia (no requiere de ningún parámetro).
- Sin embargo también es posible decirle al contenedor de Laravel cosas como «cuando se requiera un objeto de esta interfaz, crea una instancia de esta clase con estos parámetros».
- E inclusive, podemos decirle a Laravel que use una clase determinada cuando la instancia se requiera en un controlador específico, pero que use otra clase cuando la instancia se necesite en otros contextos.
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.
Registrando nuestro servicio en el service container
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:
- Nuestro proyecto necesita usar un objeto de la clase ClaseA.
- Pero, necesitamos una instancia de ClaseA con ciertos parámetros (bien específicos).
- Se requiere de una instancia de ClaseB (que depende de una variable de configuración).
- Se requiere de una instancia de ClaseC (que depende a su vez de otras 2 clases).
- Y finalmente se requiere de un objeto de ClaseD, instanciado con 2 parámetros en específico.
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.
Usando nuestro servicio
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()
}
}
Ejemplo de refactorización
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:
- Se usa el método
file
de la claseRequest
de Laravel, para obtener un objeto con información del archivo subido - Se obtiene la extensión del archivo
- Se genera un id único basado en la hora actual del sistema
- Se genera un nombre de archivo usando el id único pero conservando la extensión
- Se sube el archivo a s3
- Si la subida fue exitosa y la extensión es
.pdf
se usa una instancia dePdfParser\Parser
para obtener el texto del archivo - Si la extensión no es
.pdf
se asume que es.doc
o.docx
, y se usa la claseDocumentParser
(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áticossiempre que sea posible, por diversas razones). - Por último registramos el texto extraído del documento en la BD, asociando el registro con el usuario que ha subido el CV y el usuario al que le pertenece
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;
}
}
Finalmente resumimos
- Un
service provider
es un proveedor de servicios, y como tal se encarga de registrar nuestros servicios ante elservice container
. - Si un servicio no tiene ninguna dependencia ni se asocia a ninguna interfaz, no se necesita de ningún
service provider
porque la inyección de dependencias funcionará de todas formas (Laravel lo resuelve usando reflection).
Te esperamos en los siguientes artículos en donde hablaremos mas acerca de estos temas, los cuales hoy en día son de vital importancia en el mundo de la tecnología.