¿Te gustaría aprender Frameworks .NET Empresariales?
Tenemos los cursos que necesitas.¡Haz clic aquí!


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 un service providerdebe 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 clase Request 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 de PdfParser\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 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á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 el service 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.

¿Te gustaría aprender Frameworks .NET Empresariales?
Tenemos los cursos que necesitas.¡Haz clic aquí!
About Author

NGuerrero

0 0 votos
Article Rating
Suscribir
Notificar de
guest
0 Comments
Comentarios.
Ver todos los comentarios
0
¿Te gusta este articulo? por favor comentax