Leo

Sobre tecnologia...

Laravel, listando itens de planilhas do Google

2 years ago · 6 MIN READ
#PHP  #Laravel  #Google Cloud Platform 

Introdução

É muito comum o cenário onde precisamos consumir alguma fonte de dados em formato CSV ou planilha do excel, como alternativa é possível consumir uma planilha armazenada no google drive, isso eliminará um fluxo de importação/exportação da planilha, facilitará o trabalho colaborativo uma vez que todos podem editar a mesma planilha. Vamos desenvolver a aplicação teste utilizando a versão 5.7 do Laravel, que foi lançada no mesmo dia em que escrevi esse artigo, para obter uma instalação fresh do Laravel siga as instruções da documentação

API e Permissões

O primeiro passo é criar um projeto dentro do Google Cloud Platform, para isso clique no botão "Selecione um projeto" ao lado do título da página, como mostrado na figura abaixo.

google-cloud-platform.png

Como eu já tenho projetos criados ele me abre um modal contendo todos os projetos e a opção desejada de "Novo Projeto" no canto superior direito, pode ser que apareça algo ligeiramente diferente para usuários que ainda não têm projetos criados, procure uma opção similar.

novo-projeto.png

Selecione um nome para o seu projeto.

novo-projeto-nome.png

Nesse momento o seu projeto já deve estar criado, selecione o projeto recém criado e prossiga em "ativar apis e serviços".

ativar-apis.png

Você deve ativar a API do "Google Sheets" pesquisando por "Google Sheets" e entrando na opção correta, após isso aperte o botão "Ativar".

ativar-google-sheets-api.png

Agora com o projeto criado e as APIs ativadas devemos criar as devidas credenciais para que nossa aplicação consiga se autenticar e consumir as APIs, para isso vá em "Credenciais" > "Criar credenciais" > "Chave da conta de serviço".

criar-credenciais.png

Como você ainda não tem uma conta de serviço para esse projeto, você deve selecionar "Nova conta de serviço" no primeiro campo, escolha um nome que faça sentido e escolha o papel de "Editor", dentro de "Projeto", selecione também o tipo de chave json.

criar-chave-da-conta-de-servico.png

Agora toda a parte de configuração das APIs está feita, para finalizar precisamos criar uma planilha no drive e dar permissão para a conta de serviço que criamos colaborar nessa planilha, para fazer isso podemos abrir o arquivo json baixado e procurar o "client_email", pra esse email que daremos a permissão de editar a planilha.

permissao-da-planilha.png

Vamos preencher a planilha com alguns itens:

planilha-pessoas.png

Aplicação

Com a aplicação instalada utilizaremos o pacote asimlqt/php-google-spreadsheet-client para consumir a planilha e o pacote google/apiclient para fazer a autenticação. Adicione as dependências ao seu composer.json e depois rode o comando "composer update"

{
    "require": {
        "asimlqt/php-google-spreadsheet-client": "3.0.*",
        "google/apiclient": "^2.2"
    }
}

Vamos dividir a aplicação em 2 partes, código que retorna o access token e uma classe que consome a planilha de fato, o código do access token deverá ser executado e a classe "injetada" de acordo com a necessidade, para que isso seja feito com facilidade vamos utilizar o service container do Laravel.

Código do access token

Vamos adicionar o código abaixo no metodoregister() do AppServiceProvider

$this->app->singleton('access.token', function () {
    return cache()->remember('spreadsheet', 60 * 24, function () {
        putenv('GOOGLE_APPLICATION_CREDENTIALS=' . storage_path('app/credentials.json'));
        $client = new \Google_Client;
        $client->useApplicationDefaultCredentials();

        $client->setApplicationName('App Test');
        $client->setScopes([
            'https://spreadsheets.google.com/feeds'
        ]);

        if ($client->isAccessTokenExpired()) {
            $client->refreshTokenWithAssertion();
        }

        return $client->fetchAccessTokenWithAssertion()['access_token'];
    });
});

Com o código acima, sempre que precisarmos de um access token, para qualquer tipo de serviço do google, chamaremos o metodo resolve() passando a string "access.token" como parâmetro, isso deve retornar o access token.

Note que na primeira linha existe um cache de 24 horas(60 * 24 minutos), essa é uma boa idéia porque evita que a aplicaçãao faça uma requisição externa sempre que o access token for requisitado por um serviço.

No código do access token é possível notar a seguinte linha:

putenv('GOOGLE_APPLICATION_CREDENTIALS=' . storage_path('app/credentials.json'));

Utilizar a função putenv() tem um efeito similar a adicionar o path das credenciais no arquivo .env, lembrando que para essa linha funcionar você deve mover o arquivo .json do google para dentro da pasta storage/app com o nome credentials.json.

É importante separar o código mostrado do código que consome a planila, se decidirmos fazer uma classe que cria documentos no Drive do Google podemos reutilizar o código para essa classe também, a única coisa que deveriamos fazer é habilitar a API do Drive e adicionar o scope adequado no método setScopes(), ficaria dessa forma:

$client->setScopes([
    'https://www.googleapis.com/auth/drive',
    'https://spreadsheets.google.com/feeds'
]);

O código que retorna o access token está pronto, agora já deve ser possível obter um resultado similar à esse pelo tinker

tinker-access-token.png

Execute a função resolve 2 vezes seguidas, repare que da primeira vez o retorno demorou alguns segundos, e da segunda vez foi instantâneo, essa é a economia de tempo que o cache nos trás.

Classe que consome a planilha

Vamos concentrar a lógica de consumo da planilha dentro de uma classe chamada SheetService que tem o objetivo de resumir a complexidade da utilização do pacote asimlqt/php-google-spreadsheet-client deixando a interface mais simples e implementando cache para evitar consumo de um webservice externo desnecessáriamente.

<?php

namespace App\Services;

use Google\Spreadsheet\Worksheet;
use Google\Spreadsheet\SpreadsheetService;
use Google\Spreadsheet\ServiceRequestFactory;
use Google\Spreadsheet\DefaultServiceRequest;
use Illuminate\Support\Collection;

class SheetService
{
    protected $collection;
    protected $spreadsheetService;

    public function __construct(SpreadsheetService $spreadsheetService)
    {
        $this->spreadsheetService = $spreadsheetService;
        $this->setAccessToken();
        $this->setCachedCollection();
    }

    protected function setAccessToken()
    {
        $accessToken = resolve('access.token');
        ServiceRequestFactory::setInstance(
            new DefaultServiceRequest($accessToken)
        );
    }

    protected function setCachedCollection()
    {
        $this->collection
            = cache()->remember('spreadsheet-collection', 60 * 24, function () {
                return $this->combineHeaders();
            });
    }

    protected function combineHeaders()
    {
        $cellFeed = $this->getFromApi();
        $headers = array_shift($cellFeed);
        return collect(array_map(function ($cell) use ($headers) {
            return array_combine($headers, $cell);
        }, $cellFeed));
    }

    protected function getFromApi()
    {
        return $this->spreadsheetService->getSpreadsheetFeed()
            ->getByTitle('app-teste')
            ->getWorksheetFeed()
            ->getByTitle('Página1')
            ->getCellFeed()
            ->toArray();
    }

    public function get()
    {
        return $this->collection;
    }

    public function find($id)
    {
        return $this->get()
            ->where('Id', $id)
            ->first();
    }
}

Em resumo, o construtor da classe acima inicializa a classe setando uma instancia de \Google\Spreadsheet\DefaultServiceRequest com o access token retornado, além de cachear uma coleção construída a partir do retorno da API:

O método getFromApi faz a consulta da planilha de fato e retorna um array, nele precisamos especificar o título e a aba da planilha através dos métodos getByTitle()

O método combineHeaders constrói uma instancia de \Illuminate\Support\Collection contendo o cabeçalho da planilha como índices dos elementos.

A propriedade collection sempre vai conter a instância de \Illuminate\Support\Collection retornada do cache, isso é importante porque torna o código rápido e permite a manipulação da coleção utilizando uma interface muito simples e semelhante à do Eloquent.

Todos os métodos públicos da Classe são construídos sobre a propriedade $collection garantindo que o cache sempre será utilizado.

Agora a classe já está pronta, é possível obter resultados iguais a esses chamando os métodos get e find pelo tinker:

sheet-service-get.png

O método find recebe o id da pessoa específica, e retorna apenas o registro dela?

sheet-service-find.png

Aplicabilidade

Vamos construir uma simples API sobre essa classe que criamos, primeiro vamos criar as seguintes rotas:

Route::get('/pessoas', 'AppController@index');
Route::get('/pessoas/{id}', 'AppController@show');

Com o comando php artisan make:controller AppController podemos gerar uma controller e depois implementar os métodosindex e show da seguinte forma:

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Services\SheetService;

class AppController extends Controller
{
    public function index(SheetService $sheetService)
    {
        return $sheetService->get();
    }

    public function show($id, SheetService $sheetService)
    {
        return $sheetService->find($id);
    }
}

A partir de agora temos uma simples API funcionando, apenas para enfatizar o consumo da planilha, se entrarmos no endpoint /pessoas devemos ver algo similar ao seguinte:

index-api-endpoint.png

Desvantagens

Caso a sua planilha comece a conter milhares de linhas pode se tornar inviável trafegar isso pela rede enquanto o usuário espera, nesse caso um banco de dados seria mais adequado. Outra situação que pode ser desajeitada é a necessidade de normalizar dados, uma vez que a aplicação não controla o que entra na planilha, essa normalização deverá ser implementada na planilha, fugindo completamente do versionamento e tornando o processo propício a erros.

Conclusão

Essa abordagem contempla a manipulação de planilhas simples e facilita a implementação do sistema evitando a necessidade de um banco de dados, bem como um fluxo de importação/exportação para alimentar esse banco.

···

Leonardo Lemos


comments powered by Disqus


Proudly powered by Canvas · Sign In