Laravel

Trabalhando com Repository no Laravel

Por em

Um dos pontos que é de extrema importância quando estamos desenvolvendo qualquer aplicação que possui uma perspectiva de crescer, é separar corretamente as responsabilidades de suas camadas, para evitar que regras de negócio, consultas personalizadas, entre outros sejam criadas em locais que gerem um alto grau de acoplamento e grandes dependências do domínio de sua aplicação em relação ao framework utilizado.

Muitas vezes, codificar algo inocente como abaixo, pode gerar uma grande dor de cabeça futura:

<?php

namespace App\Http\Controllers;

class ClientController extends Controller
{

    public function list()
    {
    	$client = new App\Client();
    	return $client->all();
    }

}

Perceba que no exemplo acima, estamos consultando diretamente um model em nosso controller.

Mas que mal há nisso?

Sinceramente, se sua aplicação for pequena, e dependendo de seu contexto, absolutamente NENHUM.

Agora, imagine que futuramente você quer mudar a regra de como listar todos os clientes, imagine que isso agora faz parte de uma regra de negócio de sua aplicação, e que tal método será consumido por diversas partes de seu sistema… e que talvez, eventualmente, você pretenda utilizar tais regras em outra aplicação feita em outro framework…

Chamar os models do Laravel diretamente em seu controller e em outras classes de seu sistema não é errado… mas dependendo do contexto, isso pode se tornar o seu pior pesadelo.

Com a grande gama de opções de frameworks (não apenas os fullstack), criar aplicações mais agnósticas possíveis poder ser uma grande saída, uma vez que o “core” de seu negócio não ficará dependente de framework, mas sim apenas de suas próprias classes.

Assumo que com a grande quantidade de recursos fornecida pelos frameworks, criar algo agnóstico pode ser um grande desafio, pois sempre haverá aquela lib do framework X que vai poupar muito de seu trabalho…

Um dos caminhos para lhe ajudar nesse processo, é trabalhar com o pattern Repository.

A idéia é bem simples: criar uma camada entre seu Model e sua aplicação para que ao invés de chamarmos diretamente o Model, possamos chamar tal camada.

Deixe-me dar um exemplo claro e simples:

<?php

namespace App\Repositories;

use App\Client;

class ClientRepository 
{
	private $model;

	public function __construct(Client $model)
	{
		$this->model = $model;
	}

	public function findAll()
	{
		return $this->model->all();
	}    
}
<?php

namespace App\Http\Controllers;

use App\Repositories\ClientRepository;

class ClientController extends Controller
{
    public function list(ClientRepository $repository)
    {
    	return $repository->findAll();
    }
}

Veja que no exemplo acima, ao invés de chamarmos o Model diretamente, estamos chamando nosso Repository, logo, meu Controller ou qualquer outra classe que precise listar os dados de um cliente, NÃO precisa ter conhecimento de nosso Model… Nesse ponto, poderíamos estar trabalhando com o Doctrine ao invés do Eloquent e nossa aplicação jamais saberia.

Obviamente, para que tenhamos a flexibilidade esperada, nosso repositório deve implementar uma interface para desacoplarmos totalmente o mesmo das outras classes.

(OBS: Quer aprender mais sobre Laravel, AngularJS e muitos outros Patterns? Conheça meu curso de Laravel 5.1 com Angular, clique aqui.)

<?php

namespace App\Repositories\Contracts;

inteface ClientRepositoryInterface
{
	public function findAll();
}


<?php

namespace App\Repositories;

use App\Repositories\Contracts\ClientRepositoryInterface;
use App\Client;

class ClientRepositoryEloquent implements ClientRepositoryInterface
{
	private $model;

	public function __consutruct(Client $model)
	{
		$this->model = $model;
	}

	public function findAll()
	{
		return $this->model->all();
	}    
}

<?php

namespace App\Repositories;

use App\Repositories\Contracts\ClientRepositoryInterface;
use App\Client;

class ClientRepositoryOutroORM implements ClientRepositoryInterface
{
	private $model;

	public function __construct(Client $model)
	{
		$this->model = $model;
	}

	public function findAll()
	{
		return $this->model->buscarRegistros();
	}    
}

Perceba que para exemplificar, criei dois Repositories:

  • ClientRepositoryEloquent
  • ClientRepositoryOutroORM

O primeiro, utiliza o Eloquent o segundo, qualquer outro ORM.

A grande sacada é que ambos implementam a mesma interface, logo, podemos simplesmente fazer isso em nosso controller:

<?php

namespace App\Http\Controllers;

use App\Repositories\Contracts\ClientRepositoryInterface;

class ClientController extends Controller
{
    public function list(ClientRepositoryInterface $repository)
    {
    	return $repository->findAll();
    }
}

Prontinho!

Agora, meu Controller sabe que receberá no método “list” um ClientRepositoryInterface, logo, para ele tanto faz qual ORM, Banco de dados, etc que será utilizado.. O que importa é que sabemos que ele possui o método findAll() que é utilizado para listar todos os clientes.

Independente de framework, o mais importante nesse caso é você entender o conceito dessa camada e como ela pode ser útil para flexibilizar sua aplicação e não deixá-la dependente de uma implementação concreta.

Talvez, nesse momento, você deva estar pensando: Mas como Laravel saberá qual Repository deverá escolher ao instanciar nosso Controller?

Na realidade ele NÃO sabe, todavia, você poderá configurar isso facilmente em seu Container de Injeção de Dependências, veja:

<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
   
    public function register()
    {
        // Para chamar ClientRepositoryEloquent
        $this->app->bind(
            'App\Repositories\Contracts\ClientRepositoryInterface', 
            'App\Repositories\ClientRepositoryEloquent'
        );

        // Para chamar ClientRepositoryOutroORM
        // $this->app->bind(
        //     'App\Repositories\Contracts\ClientRepositoryInterface', 
        //     'App\Repositories\ClientRepositoryOutroORM'
        // );
    }
}

No exemplo acima, estamos dizendo para o Container do Laravel o seguinte: Quando você tiver que instanciar um objeto que possua a dependência dessa Interface: App\Repositories\Contracts\ClientRepositoryInterface, instancie a seguinte classe concreta: App\Repositories\ClientRepositoryEloquent.

Pronto!

Se em algum momento do processo você quiser mudar de ORM, basta você trocar a chamada do App\Repositories\ClientRepositoryEloquent para o Repository que você quiser, que automaticamente o Laravel se encarregará do restante e você não precisará mudar nenhuma linha de código onde você faz uso de tal recurso.

Como muitos sabem, sou um super fâ do Doctrine, logo, resolvi criar uma classe Abstrata de repository baseada na do Doctrine para o Laravel, dessa forma, teremos recursos muito úteis (Doctrine like).

Veja a classe abaixo:

<?php

namespace App\Repositories;

abstract class AbstractRepository
{
    /**
     * @var \Illuminate\Database\Eloquent\Model
     */
    protected $model;

    public function find($id)
    {
        return $this->model->find($id);
    }

    public function findAll()
    {
        return $this->model->all();
    }

    public function create(array $data)
    {
        return $this->model->create($data);
    }

    public function update(array $data, $id)
    {
        return $this->model->find($id)->update($data);
    }

    public function firstOrCreate(array $data)
    {
        return $this->model->firstOrCreate($data);
    }

    public function delete($id)
    {
        return $this->model->find($id)->delete();
    }

    public function findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
    {
        $model = $this->model;

        if (count($criteria) == 1) {
            foreach ($criteria as $c) {
                $model = $model->where($c[0], $c[1], $c[2]);
            }
        } elseif (count($criteria > 1)) {
            $model = $model->where($criteria[0], $criteria[1], $criteria[2]);
        }

        if (count($orderBy) == 1) {
            foreach ($orderBy as $order) {
                $model = $model->orderBy($order[0], $order[1]);
            }
        } elseif (count($orderBy > 1)) {
            $model = $model->orderBy($orderBy[0], $orderBy[1]);
        }

        if (count($limit)) {
            $model = $model->take((int)$limit);
        }

        if (count($offset)) {
            $model = $model->skip((int)$offset);
        }

        return $model->get();
    }

    public function findOneBy(array $criteria)
    {
        return $this->findBy($criteria)->first();
    }

    // from Doctrine
    public function __call($method, $arguments)
    {
        if (substr($method, 0, 6) == 'findBy') {
            $by = substr($method, 6, strlen($method));
            $method = 'findBy';
        } else {
            if (substr($method, 0, 9) == 'findOneBy') {
                $by = substr($method, 9, strlen($method));
                $method = 'findOneBy';
            } else {
                throw new \Exception(
                    "Undefined method '$method'. The method name must start with " .
                    "either findBy or findOneBy!"
                );
            }
        }
        if (!isset($arguments[0])) {
            // we dont even want to allow null at this point, because we cannot (yet) transform it into IS NULL.
            throw new \Exception('You must have one argument');
        }

        $fieldName = lcfirst($by);

        return $this->$method([$fieldName, '=', $arguments[0]]);
    }

    public function paginate($pages)
    {
        return $this->model->paginate($pages);
    }
}

Fazendo seu Repository estender dessa classe, você poderá realizar diversas chamadas como:

<?php

namespace App\Http\Controllers;

use App\Repositories\Contracts\ClientRepositoryInterface;

class ClientController extends Controller
{
    public function list(ClientRepositoryInterface $repository)
    {

    	// Buscar todos os Clientes pelo nome
    	$clientByName = $repository->findBy(['name'=>'José']);

    	// Buscar o primeiro com o nome de josé
    	$clientByName = $repository->findOneBy(['name'=>'José']);

    	// ou ainda utilizando o método mágico __call
		$clientByName = $repository->findByName('José');
		// ou
		$clientByName = $repository->findOneByName('José');

		$clientByIdade = $repository->findByIdade(15);

		// listar com mais de uma condição
		$client = $repository->findBy(['name'=>'José', 'idade'=>20]);

		// ordenando
		// listar com mais de uma condição
		$client = $repository->findBy(['name'=>'José', 'idade'=>20], ['name'=>'desc']);

		// paginar
		$clients = $repository->paginate(20);

		//criar
		$client = $repository->create($data);

		// buscar todos
		$client = $repository->findAll();

		// buscar por Id
		$client = $repository->find(1);
    }
}

Se você quiser aprender ainda mais sobre Repository e outros padrões do jeito certo, conheça o novo curso: Laravel 5 com Angular JS + AWS. Clique aqui e saiba mais.