Suponha que você tenha um pacote para aplicações Laravel ou ainda um módulo de sua aplicação que é desenvolvido independente dela.
Como tratar em seu próprio pacote/módulo determinadas exceções que o framework ou a aplicação podem gerar, substituindo o exception handler deles quando for conveniente e ao mesmo tempo delegando a eles exceções que você não irá tratar? 🤔
Aplicando o Decorator pattern. 😄
Observe esta implementação de pacote:
src/Exceptions/Handler.php
<?php
namespace Package\Exceptions;
use Exception;
use Illuminate\Contracts\Debug\ExceptionHandler as HandlerContract;
class Handler implements HandlerContract
{
/**
* The exception handler being decorated.
*
* @var \Illuminate\Contracts\Debug\ExceptionHandler
*/
protected $exceptionHandler;
/**
* Create a new exception handler instance.
*
* @param \Illuminate\Contracts\Debug\ExceptionHandler $exceptionHandler
* @return void
*/
public function __construct(HandlerContract $exceptionHandler)
{
$this->exceptionHandler = $exceptionHandler;
}
/**
* Report or log an exception.
*
* @param \Exception $exception
* @return void
*
* @throws \Exception
*/
public function report(Exception $exception)
{
// Handle whatever you want...
// Fallback to app handler
$this->exceptionHandler->report($exception);
}
/**
* Render an exception into a response.
*
* @param \Illuminate\Http\Request $request
* @param \Exception $exception
* @return \Symfony\Component\HttpFoundation\Response
*/
public function render($request, Exception $exception)
{
// Handle whatever you want...
// Fallback to app handler
return $this->exceptionHandler->render($request, $exception);
}
/**
* Render an exception to the console.
*
* @param \Symfony\Component\Console\Output\OutputInterface $output
* @param \Exception $exception
* @return void
*/
public function renderForConsole($output, Exception $exception)
{
// Handle whatever you want...
// Fallback to app handler
$this->exceptionHandler->renderForConsole($output, $exception);
}
}
src/ServiceProvider.php
<?php
namespace Package;
use Illuminate\Contracts\Debug\ExceptionHandler as HandlerContract;
use Illuminate\Support\ServiceProvider as AbstractServiceProvider;
use Package\Exceptions\Handler;
class ServiceProvider extends AbstractServiceProvider
{
/**
* Bootstrap the package services.
*
* @return void
*/
public function boot()
{
//
}
/**
* Register the package services.
*
* @return void
*/
public function register()
{
$this->app->extend(HandlerContract::class, function ($appHandler) {
return new Handler($appHandler);
});
}
}
Para facilitar, este código foi disponibilizado em um Gist: https://gist.github.com/paulofreitas/6ba4c12f4098931ec0ca8f8ae934e569
Achei interessante compartilhar isso num post próprio porque isso pode ser útil em vários outros cenários onde você precise substituir uma implementação concreta de algum componente do Laravel ao mesmo tempo em que você possa depender dela - coisa que o método bind()
do IoC container não possibilitaria em casos como este onde a própria aplicação oferece a implementação concreta.
Quem viabiliza esta mágica é o método extend()
do container (que ainda não foi documentado). Ao chamar este método passando uma interface de contrato, recebemos dele a implementação daquele contrato e podemos com isso usar o conceito de decoração para passar essa implementação para o construtor da nova implementação ao mesmo tempo em que substituímos ela. 😀
Isso permite que a nova implementação de um dado contrato possa delegar determinados métodos para a implementação que já existia aplicando o conceito do Chain of Responsibility pattern. No caso demonstrado, se o exception handler do package não tratar uma dada exceção, ele irá delegar isso para o handler superior que foi injetado através do construtor por meio do conceito de decoração lá no extend()
do container. Este handler superior geralmente será o exception handler da própria aplicação, que por sua vez se não tratar a exceção também irá delegar ela para o handler superior, que no caso será o do próprio framework.
Especificamente esta parte de decorar uma implementação ao substituir ela no container pode ser útil em várias outras situações em que você precise modificar o comportamento de algum componente do framework cuja implementação concreta dependa da aplicação.
Porque as maravilhas da inversão de controle do container vão muito além dos binds, singletons e aliases. 😁