Exportando dados em planilhas no Laravel e Livewire sem sujeira e sem raizes brancas
Tags: php, laravel, livewire, laravelexcel
Published: 2021-09-15 00:48:17

Esses dias eu tive que criar "só um botãozinho" que exportasse os dados filtrados pelo usuário na página e exportasse para uma planilha para ser aberta no Excel. E depois de uma boa pesquisa, encontrei o pacote [Laravel Excel](https://laravel-excel.com/), que facilita muito o trabalho. Neste artigo quero compartilhar minha experiência na implementação, pois apesar de o pacote ter [documentação com exemplos](https://docs.laravel-excel.com/3.1/getting-started/), tive um pouco de trabalho pra juntar tudo e fazer funcionar da forma que eu precisava.

## Antes de começar
O projeto que estou trabalhando usa [Livewire](https://laravel-livewire.com/) rodando em Laravel 8 e PHP 8.0 e por isso os exemplos de código contidos no artigo usarão estas tecnologias, mas o pacote não depende do Livewire, podendo ser usado com Laravel a partir da versão 5.8, e com versões do PHP a partir da versão 7.2.

## Configuração

### Instalando o pacote
O primeiro passo é instalar o pacote através do composer.
```bash
composer require maatwebsite/excel
```
### Criando a classe de exportação
Com o pacote instalado, o próximo passo é criar a classe exportadora, que vai cuidar de configurar o arquivo a ser exportado. O comando que cria a classe de exportação aceita o parâmetro `--model`, usado para informar o *Model* de onde sairão as informações.
```bash
php artisan make:export TransactionExport --model=AccountTransaction
```
A classe gerada pelo comando do `artisan` terá a seguinte estrutura:
```php
 <?php

namespace App\Exports;  

use App\Models\AccountTransaction;  
use Maatwebsite\Excel\Concerns\FromCollection;  

class TransactionExport implements FromCollection  
{  
    public function collection()
    {
        return AccountTransaction::all();
    }

}
```
 
## Utilização

### Passando dados para a exportação
Por padrão, a classe de exportação seleciona todos os registros do *Model* para exportar, mas é possível passar outra fonte de dados, que pode ser uma *Collection* ou um *array* através do método mágico `__constructor` da classe. No exemplo abaixo, é feito com uma collection
```php
public function __construct(
	public Collection $transactions
) {
}

public function collection(): Collection
{
	return $this->transactions;
}
```

### Adicionando o cabeçalho
Apesar de neste ponto já ser possível baixar o arquivo, ele ainda não vai ter cabeçalho, mas o pacote permite adicionar cabeçalho ao arquivo utilizando a interface do pacote Laravel Excel `WithHeadings` à chamada da classe:
```php
class TransactionExport implements FromCollection, WithHeadings
{
```
Depois disso, precisamos passar os dados do cabeçalho no `__constructor` através de um *array*
```php
public function __construct(
	public Collection $transactions,
	public array $headings  
) {  
}
```
Eu optei por usar os nomes das colunas como estão no banco de dados no cabeçalho e porque poderia reaproveitar esta informação para selecionar as colunas no método de exportação
```php
$columns  =  [
	'description',
	'financial_category_id',
	'transaction_status_id',
	'accrual_month',
	'due_date',
	'value',
	'document_number',
	'notes',
	'financial_account_id',
	'supplier_id',
	'cost_center_id',
	'payment_date',
	'final_value',
	'payment_method_id'
];
```
Mas como estes nomes de colunas não seriam bons, criei entradas respectivas no arquivo de tradução `resources\lang\pt_BR\dashboard.php`:
```php
return [
	...
	'account_transaction' => [  
		'description' => 'Descrição',  
		'financial_category_id' => 'Categoria Financeira',  
		'transaction_status_id' => 'Status da Transação',  
		'accrual_month' => 'Mês de Vigência',  
		'due_date' => 'Data de Vencimento',  
		'value' => 'Valor',  
		'document_number' => 'Número do Documento',  
		'notes' => 'Observações',  
		'financial_account_id' => 'Conta Financeira',  
		'supplier_id' => 'Fornecedor',  
		'cost_center_id' => 'Centro de Custo',  
		'payment_date' => 'Data do Pagamento',  
		'final_value' => 'Valor Recebido',  
		'payment_method_id' => 'Forma de Pagamento'
	]
];
```
E usando o método `headings` da interface do pacote Laravel Excel `WithHeadings`, usei a função `array_map` para fazer com que o array de nomes do cabeçalho fosse traduzido:
```php  
public function headings(): array  
{ 
	return array_map(  
		fn($columnName) => __('dashboard.account_transaction.'.$columnName),  
		$this->headings  
	);  
}
```
### Personalizando a planilha
Nosso trabalho está quase completo, mas tanto os dados quanto a aparência da planilha podem ser melhor exibidos, e é isso que faremos a seguir.

#### Formatando dados
Os vêm do jeito que estão no banco de dados, ou seja, com os IDs das propriedades que têm relacionamento e as datas "cruas", mas podemos formatar estes dados com as interfaces do pacote Laravel Excel `ColumnFormatting` e `WithMapping`
```php
class TransactionExport implements
	FromCollection,  
    WithColumnFormatting,  
    WithMapping,  
    WithHeadings
{
```
E implementamos seus métodos. Começando pelo `map`, para ordenar e substituir os IDs pelos respectivos nomes das entidades relacionadas com as colunas:
```php
use PhpOffice\PhpSpreadsheet\Shared\Date;

...

public function map($row): array  
{  
  return [  
  $row->description,  
  $row->financialCategory->name,  
  $row->transactionStatus->name,  
  Date::dateTimeToExcel($row->accrual_month),  
  Date::dateTimeToExcel($row->due_date),  
  $row->value,  
  $row->document_number,  
  $row->notes,  
  $row->financialAccount->name,  
  $row->supplier->name,  
  $row->costCenter?->name,  
  $row->payment_date ? Date::dateTimeToExcel($row->payment_date) : null,  
  $row->final_value,  
  $row->paymentMethod?->name,  
  ];  
}
```
A classe `PhpOffice\PhpSpreadsheet\Shared\Date` é um "bônus" que o pacote Laravel Excel nos dá para facilitar a manipulação de dados. Como o pacote tem dependência com o [PhpSpreadsheet](https://phpspreadsheet.readthedocs.io/en/latest/), podemos usar métodos deste pacote.

O segundo passo para ajustar as linhas da planilha é resolvida pelo método `columnFormats`, que ficará assim:
```php
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;

...

public function columnFormats(): array  
{
	return [  
	    'D' => 'mm/yyyy',  
	    'E' => NumberFormat::FORMAT_DATE_DDMMYYYY,  
	    'F' => NumberFormat::FORMAT_CURRENCY_USD_SIMPLE,  
	    'L' => NumberFormat::FORMAT_DATE_DDMMYYYY,  
	    'M' => NumberFormat::FORMAT_CURRENCY_USD_SIMPLE,  
    ];  
}
```
Como é visto no código de exemplo, o array deve ser composto pela letra que corresponde à coluna que precisamos formatar e uma máscara com a regra de formatação. Aqui mais uma vez usamos um método do pacote *PhpSpreadsheet*. Neste caso, o `PhpOffice\PhpSpreadsheet\Style\NumberFormat`, que tem inúmeras máscaras para formatação, mas você pode usar uma personalizada, assim como eu fiz na primeira entrada do array retornado pelo método.

#### Estilizando o cabeçalho
Agora só falta destacar o cabeçalho para que nossa planilha fique pronta para ser exportada e lida. E o Laravel Excel tem uma Interface para nos ajudar: `WithStyles`:
```php
class TransactionExport implements  
    FromCollection,  
    WithColumnFormatting,  
    WithMapping,  
    WithHeadings,  
    WithStyles
{
```
Com a interface adicionada, hora de implementar o método `styles`:
```php
use PhpOffice\PhpSpreadsheet\Style\Fill;

...

public function styles(Worksheet $sheet)  
{  
  $sheet->getStyle('A1:N1')
		->getFill()  
		->setFillType(Fill::FILL_SOLID)  
		->getStartColor()  
		->setRGB('51d2b7');  
  
  $sheet->getStyle('A1:N1')  
		->getFont()  
		->setBold(true)  
		->getColor()  
		->setRGB('ffffff');  
}
```
Aqui, a primeira declaração é responsável por estilizar a cor de fundo do cabeçalho, e a segunda é responsável por formatar a fonte, deixando o cabeçalho com o fundo verde e a fonte em negrito e branca. Além destes atributos, também é possível mudar outros como borda, alinhamento, etc. Os métodos e parâmetros obedecem os padrões da [documentação de formatação do PhpSpreadsheet](https://phpspreadsheet.readthedocs.io/en/latest/topics/recipes/#valid-array-keys-for-style-applyfromarray)

Pra arrematar, indico usar a interface `ShouldAutoSize`, que ajusta o tamanho das *colunas automagicamente*.
```php
class TransactionExport implements  
    FromCollection,  
    WithColumnFormatting,  
    WithMapping,  
    WithHeadings,  
    WithStyles,  
	ShouldAutoSize  
{
```
#### Classe de exportação
No final, a classe de exportação ficará assim:
```php
<?php

namespace App\Exports;

use App\Models\AccountTransaction;
use Illuminate\Support\Collection;
use Maatwebsite\Excel\Concerns\FromCollection;
use Maatwebsite\Excel\Concerns\ShouldAutoSize;
use Maatwebsite\Excel\Concerns\WithColumnFormatting;
use Maatwebsite\Excel\Concerns\WithHeadings;
use Maatwebsite\Excel\Concerns\WithMapping;
use Maatwebsite\Excel\Concerns\WithStyles;
use PhpOffice\PhpSpreadsheet\Shared\Date;
use PhpOffice\PhpSpreadsheet\Style\Fill;
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;

class TransactionExport implements
	FromCollection,
	WithColumnFormatting,
	WithMapping,
	WithHeadings,
	WithStyles,
	ShouldAutoSize
{
    public function __construct(
	    public Collection $transactions, public array $headings
	) {
    }
    
    public function collection()
    {
        return $this->transactions;
    }

    public function columnFormats(): array
    {
        return [
	        'D' => 'mm/yyyy',
	        'E' => NumberFormat::FORMAT_DATE_DDMMYYYY,
	        'F' => NumberFormat::FORMAT_CURRENCY_USD_SIMPLE,
	        'L' => NumberFormat::FORMAT_DATE_DDMMYYYY,
	        'M' => NumberFormat::FORMAT_CURRENCY_USD_SIMPLE,
	    ];
	}

    public function map($row): array
    {
	    return [
		    $row->description,
	        $row->financialCategory->name,
	        $row->transactionStatus->name,
	        Date::dateTimeToExcel($row->accrual_month),
	        Date::dateTimeToExcel($row->due_date),
	        $row->value,
	        $row->document_number,
	        $row->notes,
	        $row->financialAccount->name,
	        $row->supplier->name,
	        $row->costCenter?->name,
	        $row->payment_date ? Date::dateTimeToExcel($row->payment_date) : null,
	        $row->final_value,
	        $row->paymentMethod?->name,
		];
	}

	public function headings() : array
	{
		return array_map(
			fn($columnName) => __('dashboard.account_transaction.' . $columnName),
			$this->headings
		);
	}

    public function styles(Worksheet $sheet)
    {
        $sheet->getStyle('A1:N1')
            ->getFill()
            ->setFillType(Fill::FILL_SOLID)
            ->getStartColor()
            ->setRGB('51d2b7');

        $sheet->getStyle('A1:N1')
            ->getFont()
            ->setBold(true)
            ->getColor()
            ->setRGB('ffffff');
    }
}
```

### Criando o método de exportação
A partir daqui, nossa classe está pronta para receber, formatar e exportar nossos dados em uma planilha, que faremos através do Livewire com o método `runQueryBuilder`, o mesmo que busca os dados para exibir na página, com a diferença de que na chamada da página, ela é terminada com o método `paginate` para enviar dados paginados.

```php

public function runQueryBuilder()  
{
	return $this->client  
		->accountTransactions()  
		->when(
			$this->transactionType,  
			fn($q) => $q->where('transaction_type', $this->transactionType)  
		)
		->when(  
			$this->financialAccount,  
			fn($q) => $q->where('financial_account_id', $this->financialAccount)  
		)
		->when(  
			$this->timePeriod,  
			fn($q) => $q->whereDate('due_date', '>=', $this->initialDueDate)
						->whereDate('due_date', '<=', $this->finalDueDate);
		)
		->orderBy('due_date');  
}
```

E finalmente o método de exportar, que recebe os dados e chama o método estático `download` da facade `Maatwebsite\Excel\Facades\Excel` para criar e baixar a planilha.
```php
public function export(): BinaryFileDownload
{
	$columns = [  
		'description',  
		'financial_category_id',  
		'transaction_status_id',  
		'accrual_month',
		'due_date',  
		'value',  
		'document_number',  
		'notes',  
		'financial_account_id',  
		'supplier_id',  
		'cost_center_id',  
		'payment_date',  
		'final_value',  
		'payment_method_id'  
	];  
  
	$data = $this->runQueryBuilder()->get($columns);  
	$filename = 'transactions_'
		. strtolower(TransactionTypeEnum::search($this->transactionType))
		.'_'. date('Ymd_his') .'.xls';
	
	return Excel::download(
		new TransactionExport($data, $columns),
		$filename
	);  
}
```

### Criando o botão de download
Com todo o código do backend criado, agora resta criar o botão que disparará a ação:
```html
<button
	wire:click="export"
	wire:loading.attr="disabled"
	class="btn btn-primary btn-block text-uppercase">  
	<b>{{__('dashboard.Export')}}</b>
</button>
```

## Resultado
E finalmente temos nosso "só um botãozinho". Ao clicar no botão de exportar, o download será iniciado em instantes e você terá um arquivo em formato XLS, que ao ser aberto em um programa de edição de planilhas - no meu caso, usei o [Google Spreadsheets](https://docs.google.com/spreadsheets) - será exibido assim:
![Imagem da planilha concluída com estilos e formatações aplicadas](https://i.ibb.co/XtP2rFZ/planilha-pronta.png)

Você também tem a opção de [exportar o arquivo em outros formatos](https://docs.laravel-excel.com/3.1/exports/export-formats.html), mas lembre-se que alguuns deles não usa estilização, portanto parte deste trabalho será desnecessário.

## E atenção, se liga aí que é hora da revisão
Na tele-aula de hoje, vimos que o pacote Laravel Excel pode ser usado para facilitar o processo de exportação de dados em forma de planilhas. Vimos também algumas formas de manipular estes dados e estilizar a planilha para melhorar sua leitura e apresentação. Por fim, implementamos tudo em um componente Livewire que nos permitiu fazer uso dos métodos desenvolvidos com um clique, baixamos o arquivo e visualizamos este em um editor de planilhas.

Este artigo foi uma forma de compartilhar minha experiência com o Laravel Excel, que me ajudou a facilitar um processo que tenho certeza que não sou só eu que vejo como complexo. A documentação do pacote tem várias outras opções de configuração de exportação de arquivos que podem ser exploradas, mas me concentrei em demonstrar as que usei. Recomendo a quem se interessar que dê uma lida e teste os exemplos.

Por hoje é isso. Nos vemos na próxima. Até lá e bons estudos.