Implementando uma Web API com a sua aplicação usando Laravel

Por -

Não é novidade que as formas de utilizar a internet estão sempre mudando. Graças a cada novo dispositivo, tecnologia ou tendência que surge influenciando a maneira e intensidade de como utilizamos a internet. A poucos anos atrás AJAX era uma novidade, hoje temos aplicações web incríveis como por exemplo o Google Docs. Criar uma aplicação para a web que acompanhe essas mudanças é algo desafiador e talvez até impossível, mas dedicar certa atenção em "se preparar para o futuro" é algo que pode compensar muito a longo prazo.

Desktop vs Mobile Internet Use Projection

Encontrar o equilibro entre Rapid Application Development e Future-Proofing não é uma tarefa fácil. Dentre os vários aspectos que contribuem para o future proof, implementar uma Web API é uma forma eficiente de se preparar para um possível aplicativo mobile, front-end single-page, separar a camada de apresentação e tornar a aplicação mais adaptável e performática.

Como fazer

Um dos principais benefícios do PHP e do framework Laravel é a produtividade no desenvolvimento de aplicações para a web. Para evitar o trabalho dobrado, vamos ver que com um pouco mais de atenção na camada dos Controllers podemos criar uma Web API ao mesmo tempo em que desenvolvemos nossa aplicação. Vamos aprender como.

A utilização dos controllers RESTful do Laravel permite ganhar um tempo na hora de declarar as rotas e actions. Porém o ponto chave para criarmos a nossa API é não retornar View::make(...) em nossos controllers. Ao invés disso, vamos delegar a responsabilidade de preparar a Response de nossas actions a um outro serviço. Vamos chamar esse serviço de ResponseBuilder.

<?php namespace MyApp\Base;

use App;
use Request;
use Input;
use Illuminate\Support\Contracts\ArrayableInterface;

class ResponseBuilder
{
    /**
     * Builds a response using the view and the given data. It will primarily
     * render the $view file with the $data, but if the request asks for Json
     * A Json response with the same $data will be returned.
     *
     * @param  string  $view
     * @param  array   $data
     * @param  array   $viewData Data that will be passed to the View only, so it will not be visible in Json responses
     *
     * @return Illuminate\Support\Contracts\RenderableInterface a renderable View or Response object
     */
    public function render($view, $data = array(), $viewData = array())
    {
        if (Request::wantsJson() || Input::get('json', false)) {
            $response = App::make('Response')
                ->json($this->morphToArray($data));
        } else {
            $response = App::make('Response')
                ->view($view, array_merge($data, $viewData));
        }

        return $response;
    }

O método render de nosso ResponseBuilder tem uma assinatura que recebe o nome da view e as variáveis que serão disponíveis a ela de forma semelhante ao View::make. No entanto, caso o cliente esteja requisitando uma resposta json, a $view do primeiro parâmetro sera ignorada e o conteúdo de $data será servido como json. O terceiro parâmetro, $viewData, é uma array de valores que também estarão disponíveis na view, porém não vão ser servidos no caso de uma chamada por json.

    /**
     * Morph the given content into Array.
     *
     * @param  mixed   $content
     *
     * @return string
     */
    protected function morphToArray($content)
    {
        if ($content instanceof ArrayableInterface || method_exists($content, 'toArray')) {
            return $content->toArray();
        }

        if (is_array($content)) {
            foreach ($content as $key => $value) {
                $content[$key] = $this->morphToArray($value);
            }
        }

        return $content;
    }
}

morphToArray converte quaisquer objetos presentes no parâmetro para arrays. O objetivo é preparar os objetos para serem servidos como json ao cliente.

Agora que temos o nosso ResponseBuilder preparando a resposta devidamente, vamos criar a classe abstrata BaseController para injetar o ResponseBuilder na hora de preparar a resposta das actions.

<?php namespace MyApp\Base;

use Illuminate\Routing\Controller;
use App;

abstract class BaseController extends Controller
{
    /**
     * The response builder that is going to be used in order to prepare
     * action responses
     *
     * @var \Pulse\Base\responseBuilder
     */
    protected $responseBuilder;

    /**
     * Sets the responseBuilder attribute of the controller
     */
    public function __construct(responseBuilder $responseBuilder)
    {
        $this->responseBuilder = $responseBuilder;
    }

    /**
     * Make a view using the attached responseBuilder object
     *
     * @param  string  $view
     * @param  array   $data
     * @param  array   $viewData Data that will be passed to the View only, so it will not be visible in Json responses
     *
     * @return Illuminate\Support\Contracts\RenderableInterface a renderable View or Response object
     */
    protected function render($view, $data = array(), $viewData = array())
    {
        return $this->responseBuilder->render($view, $data, $viewData);
    }
}

Basicamente estamos injetando um objeto ResponseBuilder no construtor. O método render simplesmente encapsula o render do ResponseBuilder.

Com isso, podemos implementar as actions de um controller (que estenda o BaseController) do seguinte modo:

public function getIndex()
{
    $page = Input::get('page', 0);

    $posts = $this->postRepository->all($page);

    return $this->render('backend.posts.index', compact('posts'));
}

public function getShow($id)
{
    $post = $this->postRepository->findOrFail($id);

    return $this->render('backend.posts.show');
}

Por fim, devido a condicional do ResponseBuilder::render, uma requisição com o Header Accept: application/json ou com o parâmetro ?json=true na querystring vai acabar por retornar uma linda resposta json. Teoricamente, basta utilizar o ResponseBuilder::render como resposta das actions para construir uma Web API juntamente com a sua aplicação convencional.

Sugiro o Postman para testar sua Web APIs. Com ele é possível personalizar o header (inclusíve o Accept: application/json) de forma fácil e verificar se as respostas estão sendo as esperadas.

O ultimo passo é adicionar ao ResponseBuilder métodos que abstraiam o redirecionamento. Ou seja, métodos que façam um Redirect para requisições comuns, mas caso o cliente solicite por json, retornem uma mensagem de sucesso ou falha.

Uma sugestão de implementação:

/**
 * Builds a redirect response to be used when the operation has been performed
 * successfully. Also if the client accepts json, a proper json response will
 * be returned.
 *
 * @param  string  $action
 * @param  array   $data
 * @param  int     $status
 * @param  array   $headers
 * @return \Illuminate\Http\RedirectResponse
 */
public function redirectSuccess($action, $data = array(), $status = 302, $headers = array())
{
    if (Request::wantsJson() || Input::get('json', false)) {
        $response = App::make('Response')
            ->json(['status' => 'success']);
    } else {
        $response = App::make('redirect')
            ->action($action, $data, $status, $headers);
    }

    return $response;
}

/**
 * Builds a redirect response to be used when the operation has failed. Also
 * if the client accepts json, a json containing the error message will be
 * returned.
 *
 * @param  string  $action
 * @param  array   $data
 * @param  int     $status
 * @param  array   $headers
 * @return \Illuminate\Http\RedirectResponse
 */
public function redirectFailed($action, $data = array(), $status = 302, $headers = array())
{
    if (Request::wantsJson() || Input::get('json', false)) {
        $content = [
            'status' => 'fail',
            'errors' => $this->morphToArray(array_get($data, 'errors'))
        ];

        $response = App::make('Response')
            ->json($content);
    } else {
        $response = App::make('redirect')
            ->action($action, $data, $status, $headers);
    }

    return $response;
}

Com isso, após o devido encapsulamento no BaseController uma action store ficaria assim:

public function postStore()
{
    $input = Input::all();

    $post = $this->postRepository->createNew($input);

    if (count($post->errors()) == 0) {
        return $this->redirectSuccess('Pulse\Backend\PostsController@create');
    } else {
        $data = [
            'id' => $post->id,
            'errors' => $post->errors() // i.e an error MessageBag
        ];

        return $this->redirectFailed(
            'Pulse\Backend\PostsController@edit',
            $data
        );
    }
}

Com esse conceito, em uma base bem estabelecida, é possível dar mais um passo em direção ao future proof evitando o re-trabalho e sem deixar de lado toda a rapidez do desenvolvimento com Laravel. Se você possuí planos para sua próxima aplicação, por experiência própria, uma abordagem que considere uma Web API desenvolvida em paralelo pode poupar futuras dores de cabeça.