Autopreenchimento de campos no FilamentPHP usando API
Tags: php, laravel, filamentphp, braziliandevs
Published: 2024-04-01 19:28:04

Neste artigo, vou mostrar como você pode consumir API e utilizá-la para autopreenchimento de campos no Filament, tornando assim seu formulário mais automatizado e dinâmico de forma simples e fácil. Vamos aprender, como exemplo, a autopreencher campos de endereço a partir do CEP digitado. Partiu?

&nbsp;

## Tópicos

- [Introdução](#introdução)
- [Passo a Passo](#passo-a-passo)
	- [Passo 1: Criando projeto Laravel e adicionando o FilamentPHP](#passo-1-criando-projeto-laravel-e-adicionando-o-filamentphp)
	- [Passo 2: Criando as migrations e relacionamentos](#passo-2-criando-as-migrations-e-relacionamentos)
	- [Passo 3: Criando nossa resource](#passo-3-criando-nossa-resource)
	- [Passo 4: Adicionando funcionalidade de autopreenchimento com API](#passo-4-adicionando-funcionalidade-de-autopreenchimento-com-api)
- [Conclusão](#conclusão)
- [Referências](#referencias)

&nbsp;

## Introdução

E ai pessoal, este artigo não tem o intuito de ser clean code, mas sim de mostrar no geral como o autopreenchimento é feito para que a didática fique melhor, porém você pode organizar seu código da maneira que desejar. Neste exemplo, vou estar fazendo uso da API do ViaCep e vamos utilizá-la para autopreenchimento de campos de endereço, mas isso não se limita só a ela, sinta-se livre para usar a API que preferir. Vamos começar!

&nbsp;

## Passo a passo

Neste exemplo, estamos utilizando as seguintes versões de nossas ferramentas:

- Laravel v11.x
- PHP v8.2
- FilamentPHP v3.2

> ❗ **Importante:** O uso de outras versões pode mudar a forma como o procedimento abaixo é feito.

### Passo 1: Criando projeto Laravel e adicionando o FilamentPHP

Para criar nosso projeto Laravel, podemos utilizar o seguinte comando:

```bash
composer create-project laravel/laravel example-app
```

Onde `example-app` é o nome do nosso projeto. Feito isso, dentro da pasta do projeto vamos usar os seguintes comandos, sendo o primeiro para adicionar o FilamentPHP e o outro para instalar os painéis do Filament:

```bash
composer require filament/filament:"^3.2" -W
 
php artisan filament:install --panels
```

### Passo 2: Criando nossos models, migrations e relacionamentos

Agora vamos rodar o seguinte comando para criar o nosso Model, onde `Address` é o nome do nosso model e a flag `-m` fará com que a Migration seja criada junto com o Model:

```bash
php artisan make:model Address -m
```

Dentro da nossa Migration vamos adicionar os campos que vamos utilizar na nossa tabela. Levando em conta que vamos utilizar a API do ViaCep, os campos deverão ficar mais ou menos dessa forma: 

```php

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
	/**
	* Run the migrations.
	*/
	public function up(): void
	{
		Schema::create('addresses', function (Blueprint $table) {
			$table->id();
			$table->foreignId('user_id')->constrained()->cascadeOnDelete();
			$table->string('postal_code')->nullable();
			$table->string('address')->nullable();
			$table->string('number')->nullable();
			$table->string('complement')->nullable();
			$table->string('neighborhood')->nullable();
			$table->string('city')->nullable();
			$table->string('uf')->nullable();
			$table->timestamps();
		});
	}

	/**
	* Reverse the migrations.
	*/
	public function down(): void
	{
		Schema::dropIfExists('addresses');
	}
};
```


Adicionado os campos na Migration, podemos rodar um `php artisan migrate` para que nossas tabelas sejam criadas no banco de dados.
E também, para criar um usuário para acessar nosso painel, usamos `php artisan make:filament-user`, digite um nome, um email e uma senha e use-os para fazer seu login no painel.


Agora vamos trabalhar nossos Models. Primeiro, precisamos adicionar os campos da nossa tabela na variável `$fillable`, como mostrado abaixo:

```php
<?php

namespace App\Models;
  
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
  
class Address extends Model
{
	use HasFactory;
	
	protected $fillable = [
		'user_id',
		'postal_code',
		'address',
		'number',
		'complement',
		'neighborhood',
		'city',
		'uf',
	];
}

```


Após isso, ainda dentro do nosso Model precisamos criar nossos relacionamentos. Considerando que um usuário terá somente um endereço e que um endereço está relacionado somente a um usuário, podemos adicionar uma relação do tipo **hasOne**:

> **Address.php**
```php
public function users()
{
	return $this->hasOne(User::class);
}
```

> **User.php**
```php
public function addresses()
{
	return $this->hasOne(Address::class);
}
```

### Passo 3: Criando nossa resource

Vamos agora criar nossa resource de User, para isso utilizamos o seguinte comando: 

```bash
php artisan make:filament-resource User --view --generate

// onde --view gera uma página de visualização e
// -- generate gerará nosso formulário e tabela automaticamente
```

Com isso, será criada sua resource em `àpp/Filament/Resources` já com seus campos e sua tabela gerada. Você perceberá também que nossos campos de endereço não estão nessa Resource, pois ele faz parte de outro Model e sendo assim "como vamos adicionar os campos de endereço na resource de User, se eles pertencem a outro Model?" Vamos lá, lembram da nossa relação? Então, vamos fazer o seguinte:

Podemos chamar nossa relação para dentro de um Fieldset através da flag ``->relationship()`` como mostrado no exemplo de layout abaixo:

```php

<?php

namespace App\Filament\Resources;

use App\Filament\Resources\UserResource\Pages;
use App\Models\User;
use Exception;
use Filament\Forms;
use Filament\Forms\Components\Actions\Action;
use Filament\Forms\Components\Fieldset;
use Filament\Forms\Form;
use Filament\Forms\Set;
use Filament\Notifications\Notification;
use Filament\Resources\Resource;
use Filament\Tables;
use Filament\Tables\Table;
use Illuminate\Support\Facades\Http;

class UserResource extends Resource
{
	protected static ?string $model = User::class;
	protected static ?string $navigationIcon = 'heroicon-o-users';

	public static function form(Form $form): Form
	{
		return $form
		->schema([
			Forms\Components\TextInput::make('name')
				->required()
				->maxLength(255),
			Forms\Components\TextInput::make('email')
				->email()
				->required()
				->maxLength(255),
			Forms\Components\DateTimePicker::make('email_verified_at'),
			Forms\Components\TextInput::make('password')
				->password()
				->required()
				->maxLength(255),
			Fieldset::make('address')->label('Address')
				->relationship('addresses') // chamando nosso relacionamento
				->schema([
					Forms\Components\TextInput::make('postal_code')
						->maxLength(255),
					Forms\Components\TextInput::make('address')
						->maxLength(255),
					Forms\Components\TextInput::make('number')
						->maxLength(255),
					Forms\Components\TextInput::make('complement')
						->maxLength(255),
					Forms\Components\TextInput::make('neighborhood')
						->maxLength(255),
					Forms\Components\TextInput::make('city')
						->maxLength(255),
					Forms\Components\TextInput::make('uf')
						->maxLength(255),
			]),
		]);
	}

  

	public static function table(Table $table): Table
	{
		return $table
			->columns([
				Tables\Columns\TextColumn::make('name')
					->searchable(),
				Tables\Columns\TextColumn::make('email')
					->searchable(),
				Tables\Columns\TextColumn::make('email_verified_at')
					->dateTime()
					->sortable(),
				Tables\Columns\TextColumn::make('created_at')
					->dateTime()
					->sortable()
					->toggleable(isToggledHiddenByDefault: true),
				Tables\Columns\TextColumn::make('updated_at')
					->dateTime()
					->sortable()
					->toggleable(isToggledHiddenByDefault: true),
			])
			->filters([
				//
			])
			->actions([
				Tables\Actions\ViewAction::make(),
				Tables\Actions\EditAction::make(),
			])
			->bulkActions([
				Tables\Actions\BulkActionGroup::make([
					Tables\Actions\DeleteBulkAction::make(),
				]),
			]);
	}

// restante o código da resource...

}
```

> O Fieldset não é o único componente de layout que aceita relações, em [Managing relationships](https://filamentphp.com/docs/3.x/panels/resources/relation-managers) você pode ver os campos/componentes e os tipos de relações que são compatíveis com eles.

Você pode organizar esses campos e a sua tabela da maneira como preferir, dentro da documentação na aba de Layout ([Layout de Formulário](https://filamentphp.com/docs/3.x/forms/layout/getting-started), [Layout de Tabela](https://filamentphp.com/docs/3.x/tables/layout)), você encontra componentes e exemplos que você pode usar para organizar seus dados. 

## Passo 4: Adicionando funcionalidade de autopreenchimento com API

Enfim, vamos adicionar nossa funcionalidade, para isso vamos utilizar a flag `->suffixAction` no nosso campo de CEP (`postal_code`), ela adicionará um botão no final do nosso campo que ao clicar disparará a ação que definirmos dentro dela:

```php
Forms\Components\TextInput::make('postal_code')->mask('99999-999')
	->suffixAction(
		Action::make('search')
			->icon('heroicon-o-magnifying-glass')
			->action()
	)
	->maxLength(255),
```
> - Você pode escolher outro ícone pra sua Action no site [Heroicons](https://heroicons.com/), lá você encontra todos os ícones suportados pelo Filament
> - E também usamos o método `->mask()` para fazer a máscara do nosso CEP, a máscara já respeitará o uso de somente números no campo.

Ela ficará visualmente da seguinte forma:


![suffixAction visualmente](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/p8rz41isyou0t9ipuvpt.jpeg)


Agora, dentro da nossa Action, dentro da flag `->action()` vamos criar nossa função. Então, como parâmetros da nossa função vamos usar uma variável `Set $set` (para setar valores nos outros campos) e uma variável `$state` (usada no Filament para pegar o valor atual do campo).

Assim, a primeira coisa que vamos fazer é verificar se o valor atual do nosso campo está em branco e se estiver retornar uma mensagem para o usuário. No exemplo abaixo, estamos retornando uma notificação usando o Notification do Filament:

```php
Forms\Components\TextInput::make('postal_code')->mask('99999-999')
	->suffixAction(
		Action::make('search')
			->icon('heroicon-o-magnifying-glass')
			->action(function(Set $set, $state){
				if (blank($state)) {
					Notification::make()
						->title('Postal Code is required')
						->danger()->send();
						return;
				}
			})
	)
	->maxLength(255),
```

Agora, dentro de um bloco `try catch`, podemos fazer nossa requisição. Dentro do `try` vamos adicionar uma variável `$cepData` que vai receber os dados que vierem da nossa requisição e dentro da URL da nossa requisição adicionamos nossa variável `$state` no endpoint para fazer a busca na API a partir do valor digitado no campo.

E dentro do `catch` fazemos nossa notificação ao usuário caso nossa requisição retorne algum erro.

```php
Forms\Components\TextInput::make('postal_code')->mask('99999-999')
	->suffixAction(
		Action::make('search')
			->icon('heroicon-o-magnifying-glass')
			->action(function(Set $set, $state){
				if (blank($state)) {
					Notification::make()
						->title('Postal Code is required')
						->danger()->send();
						return;
				}
				try {
					$cepData = 
						Http::get("https://viacep.com.br/ws/{$state}/json")
						->throw()->json();

				} catch (Exception $e) {
					Notification::make()
						->title('Postal Code not found')
						->danger()->send();
				}
			})
	)
	->maxLength(255),
```

Mas temos um detalhe, na API do ViaCep, ao enviarmos um CEP e ele não for encontrado ela irá retornar um array com uma chave de erro e não uma Exception (mostrado na imagem abaixo) e então não cairá no nosso bloco `catch` e o usuário não será notificado.


![Array com chave de erro](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zx5k12u8v4svv2ba3w8a.png)


Para resolver isso, temos que forçar uma Exception para que nosso bloco catch "agarre" este erro e a notificação ao usuário seja enviada, fazemos isso da seguinte forma:

```php
Forms\Components\TextInput::make('postal_code')->mask('99999-999')
	->suffixAction(
		Action::make('search')
			->icon('heroicon-o-magnifying-glass')
			->action(function(Set $set, $state){
				if (blank($state)) {
					Notification::make()
						->title('Postal Code is required')
						->danger()->send();
						return;
				}
				try {
					$cepData = 
						Http::get("https://viacep.com.br/ws/{$state}/json")
						->throw()->json();
						
					// forçando a Exception
					if (in_array('erro', $cepData)) {
						throw new Exception('Postal Code not found');
					}
					
				} catch (Exception $e) {
					Notification::make()
						->title('Postal Code not found')
						->danger()->send();
				}
			})
	)
	->maxLength(255),
```

Agora, considerando que nossa variável `$cepData`, recebeu nossos dados da requisição, utilizamos a nossa variável `$set` para setar os valores que vieram da nossa requisição nos outros campos ou `null` caso não venha nada:

```php
Forms\Components\TextInput::make('postal_code')->mask('99999-999')
	->suffixAction(
		Action::make('search')
			->icon('heroicon-o-magnifying-glass')
			->action(function(Set $set, $state){
				if (blank($state)) {
					Notification::make()
						->title('Postal Code is required')
						->danger()->send();
						return;
				}
				try {
					$cepData = 
						Http::get("https://viacep.com.br/ws/{$state}/json")
						->throw()->json();
						
					// forçando a Exception
					if (in_array('erro', $cepData)) {
						throw new Exception('Postal Code not found');
					}
					
				} catch (Exception $e) {
					Notification::make()
						->title('Postal Code not found')
						->danger()->send();
				}

				$set('address', $cepData['logradouro'] ?? null);
				$set('neighborhood', $cepData['bairro'] ?? null);
				$set('city', $cepData['localidade'] ?? null);
				$set('uf', $cepData['uf'] ?? null);
			})
	)
	->maxLength(255),
```

**Pronto!** Agora nosso autopreenchimento já está funcionando. Abaixo mostro uma demonstração da nossa funcionalidade em ação.

{% embed https://youtu.be/BFN_Q_pkx6E %}


## Conclusão

Agora que você já aprendeu como fazer o autopreenchimento, pode usar isso nos seus projetos e tornar seus formulários menos cansativos de preencher e mais dinâmicos. Esta funcionalidade é até mesmo uma forma de facilitar a vida do usuário e reduzir erros de escrita na hora de preencher o formulário. 
Abaixo vou estar disponibilizando o link do repositório do nosso exemplo, sinta-se livre para brincar como quiser. Espero que você tenha gostado e qualquer dúvida estou a disposição ^^
Até a próxima !!

&nbsp;

## Referências

- [Laravel Relationships](https://laravel.com/docs/11.x/eloquent-relationships)
- [Managing Relationships - FIlament](https://filamentphp.com/docs/3.x/panels/resources/relation-managers)
- [Filament Form Layout](https://filamentphp.com/docs/3.x/forms/layout/getting-started)
- [Repositório Filament ViaCep](https://github.com/AdryanneKelly/filament-viacep)
- [API ViaCep](https://viacep.com.br/)
- [Heroicons](https://heroicons.com/)

&nbsp;

_Obrigada @clintonrocha98 mais uma vez pelos feedbacks! 💜_