[PARTE 2] O Poder desconhecido do .yaml 🚀
Post – Parte 2 de 2
Nota: Este é a continuação do post. Se você iniciou pela Parte 1, confira esta segunda parte para ter acesso ao conteúdo completo.
Ao usar o componente abaixo:
<x-dynamic-form :formConfig="$formConfig" :selectsPopulate="$selectsPopulate" :formData="$formData" :isEdit="$isEdit" />
Eu consigo reduzir código genérico e focar no que realmente importa para o User, que seria a lógica de vincular ele a uma entidade no meu sistema.
<div class="flex justify-center w-full">
<div class="w-4/5 flex flex-col justify-center p-5">
<p wire:loading>Carregando...</p>
<form wire:loading.remove wire:submit.prevent="submitForm">
<!-- Alerta para Preenchimento dos Campos -->
<div class="flex flex-row items-center bg-white w-full container px-5 py-4 mt-2 rounded-lg">
<i class="fad fa-exclamation-triangle ml-2 text-warning-600 text-2xl"></i>
<div class="ml-3">
<p class="text-gray-500 ml-3 text-lg p-0 m-0">
Atente para os campos em <span class="text-red-400">*negrito</span>, eles são obrigatórios
</p>
</div>
</div>
<!-- Card do Formulário -->
<div class="w-full mt-6 bg-white p-5 rounded-lg">
<div class="flex flex-row items-center justify-between">
<p class="text-black/75 ml-3 font-semibold text-xl p-0 m-0">
Formuário para @php echo $isEdit ? "edição" : "criação" @endphp de {{$params['_title']}}
</p>
<button type="button" @click="window.history.back()"
class="relative p-2 mr-3 rounded-lg transition duration-300 text-primary-400 bg-primary-300/20 hover:text-white hover:bg-primary-500 hover:cursor-pointer hover:shadow-sm">
<i class="fad fa-undo text-xl p-1"></i>
</button>
</div>
<div class="border-[0.3px] mx-3 my-6 border-primary-300"></div>
<x-dynamic-form :formConfig="$formConfig" :selectsPopulate="$selectsPopulate" :formData="$formData" :isEdit="$isEdit" />
@php
$disabled = $isEdit;
$disabledClasses = $disabled ? 'opacity-50 cursor-not-allowed' : '';
@endphp
<div>
<p class="text-black/75 ml-3 font-semibold text-xl p-0 mb-5">Vínculo</p>
<div class="flex flex-row space-x-4 mx-3">
<div x-data class="mb-4 w-full">
<label for="representedAgent" class="block mb-2 text-sm font-medium text-gray-700">
Entidade Representada <span class="text-red-500">*</span>
</label>
<select wire:model.lazy="formData.representedAgent" placeholder="Selecione o Agente Representado"
id="representedAgent"
class="w-full rounded-md border border-gray-300 px-3 py-2 text-gray-700 focus:outline-none focus:ring-1 focus:ring-primary-500/30 focus:border-primary-500/30 {{ $disabledClasses }}"
@if ($disabled) disabled @endif>
<option value="">Selecionar...</option>
@foreach ($selectsPopulate['representedAgent'] as $key => $value)
<option value="{{ $key }}" {{ $formData['representedAgent'] == $key ? 'selected' : '' }}>
{{ $value }}
</option>
@endforeach
</select>
@error('formData.name')
<p class="mt-2 text-xs text-red-500 font-semibold">{{ $message }}</p>
@else
<p class="mt-2 text-xs text-secondary-500/60 font-semibold">Selecione o agente representado</p>
@enderror
</div>
</div>
</div>
</div>
<div class="flex flex-row justify-end w-full mt-6 bg-white p-5 rounded-lg">
<div class="space-x-2">
<button type="button" @click="$dispatch('back')"
class="bg-primary-200/55 text-primary-600 p-2 rounded-lg hover:bg-primary-300 hover:text-white transition hover:cursor-pointer">
<i class="fad fa-times-circle p-1"></i>
<span class="font-semibold">Cancelar</span>
</button>
<button type="submit" class="bg-primary-300 text-white p-2 px-4 rounded-lg hover:cursor-pointer">
<i class="fad fa-check-circle p-1"></i>
<span class="font-semibold">Salvar</span>
</button>
</div>
</div>
</form>
</div>
</div>
Isso torna o desenvolvimento muito rápido e o código reutilizável, mantendo o conceito de DRY (Dont Repeat Yourself) ao extremo.
Como que o Laravel e Livewire lidam com o .yaml?
Diferente do Ipanel que uso no serviço, onde as classes constroem o HTML e jQuery interpolando uma string no PHP e retornando através de um getter da classe responsável, eu preferi adotar uma dinâmica diferente. O .yaml é processado pelo Controlador YamlInterpreter, mas, ao invés de construir o HTML, prefiro construir um array associativo. Sempre que posso, implemento arrays associativos, pois é uma técnica de código que acho muito eficiente.
<?php
namespace App\Controllers;
use App\Rules\ValidateCPF;
use Symfony\Component\Yaml\Yaml;
class YamlInterpreter {
public $local = "";
public $file = "";
public $formOutput = array();
public $listOutput = array();
public $permissionsOutput = array();
public function __construct($local) {
$this->local = $local;
$this->file = base_path('core/'.$this->local.'.yaml');
}
public function getPermissionsFromConfig() {
$permissionsConfig = array();
if(file_exists($this->file)) {
$permissionsConfig = Yaml::parseFile($this->file)['Areas'];
}
foreach ($permissionsConfig as $group => $data) {
if(!isset($this->permissionsOutput[$group])) {
$this->permissionsOutput[$group] = array(
"name" => $data['name'],
"subItens" => array(),
);
}
foreach ($data['subItens'] as $area => $areaData) {
if(!isset($this->permissionsOutput[$group][$area])) {
$this->permissionsOutput[$group]['subItens'][] = array(
"name" => $areaData['name'],
"area" => $area,
"permissions" => $areaData['actions'],
);
}
}
}
return $this->permissionsOutput;
}
public function renderListUIData() {
$this->listOutput['tableConfig'] = array();
$this->listOutput['gridConfig'] = array();
$this->listOutput['additionalSingleActions'] = array();
$this->listOutput['buttonsConfig'] = array(
"showSearchButton" => true,
"showInsertButton" => true,
"showEditButton" => false,
"showDetailsButton" => false,
"showDeleteButton" => false
);
$this->listOutput['startsOn'] = "list";
$this->listOutput['viewForm'] = "list.component";
// Carregando arquivo
$listingConfig = array();
if(file_exists($this->file)) {
$listingConfig = Yaml::parseFile($this->file)[$this->local];
}
// Pegando configurações da tabela, grid e botões
if(key_exists('startsOn', $listingConfig)) {
$this->listOutput['startsOn'] = $listingConfig['startsOn'];
}
if(key_exists('listingConfig', $listingConfig)) {
foreach ($listingConfig['listingConfig'] as $type => $data) {
foreach ($data as $field => $config) {
$typeConfig = $type."Config";
$this->listOutput[$typeConfig][$field] = $config;
}
}
}
if(key_exists('buttonsConfig', $listingConfig)) {
foreach ($listingConfig['buttonsConfig'] as $button => $data) {
$this->listOutput['buttonsConfig'][$button] = $data;
}
}
if(key_exists('formConfig', $listingConfig)) {
if(key_exists('view', $listingConfig['formConfig'])) {
$this->listOutput['viewForm'] = $listingConfig['formConfig']['view'];
}
}
if(isset($listingConfig['additionalSingleActions'])) {
foreach ($listingConfig['additionalSingleActions'] as $name => $data) {
$this->listOutput['additionalSingleActions'][$name] = $data;
}
}
// Carregando o controlador dinâmicamente
$this->listOutput['getConfig'] = $listingConfig['getConfig'];
// Marcando campo do id
$this->listOutput['identifier'] = $listingConfig['identifier'];
return $this->listOutput;
}
public function renderFormUIData() {
$this->formOutput['formConfig'] = array();
$this->formOutput['selectsPopulate'] = array();
$this->formOutput['messages'] = array();
$this->formOutput['rules'] = array();
$this->formOutput['validationAttributes'] = array();
$this->formOutput['formData'] = array();
$this->formOutput['identifierToField'] = array();
$this->formOutput['remoteUpdates'] = array();
// Carregando arquivo
$formConfig = array();
if(file_exists($this->file)) {
$formConfig = Yaml::parseFile($this->file)[$this->local];
}
foreach ($formConfig['formConfig'] as $field => $data) {
if($field == "view") {
continue;
}
// Carregando configurações da UI do formulário
if(!isset($this->formOutput['formConfig'][$data['groupIn']])) {
$this->formOutput['formConfig'][$data['groupIn']] = array();
}
if(!isset($this->formOutput['formConfig'][$data['groupIn']][$data['line']])) {
$this->formOutput['formConfig'][$data['groupIn']][$data['line']] = array();
}
if($data['type'] == "select" || $data['type'] == "relation") {
if(!isset($this->formOutput['selectsPopulate'][$data['identifier']])) {
$this->formOutput['selectsPopulate'][$data['identifier']] = array();
}
if(@$data['values']) {
$this->formOutput['selectsPopulate'][$data['identifier']] = $data['values'];
}
if(@$data['fillOnStart']) {
$fill = $data['fillOnStart'];
$daoCtrl = app()->makeWith("App\\Controllers\\".$fill['controller'], $fill['params'] ?? []);
$temp = $daoCtrl->{$fill['method']}()->pluck(...array_values($fill['pluck']))->toArray();
$this->formOutput['selectsPopulate'][$data['identifier']] = $temp;
}
}
// Adicionando as validações nos campos
if(isset($data['validationRules'])) {
$validationArray = array();
foreach ($data['validationRules'] as $validation) {
if(strpos($validation, ":") !== false) {
$rule = explode(":", $validation)[0];
} else {
$rule = $validation;
}
if($rule == "required") {
$data['required'] = true;
}
$this->formOutput['messages']['formData.'.$data['identifier'].'.'.$rule] = getMessageForValidation($rule);
$validationArray[] = $validation;
}
if(@$data['customValidation']) {
$customRule = app("App\\Rules\\".$data['customValidation']);
$object = new $customRule;
}
$this->formOutput['rules']['formData.'.$data['identifier']] = $validationArray;
}
if(isset($data['updateRemoteField'])) {
$this->formOutput['remoteUpdates'][$data['identifier']] = $data['updateRemoteField'];
}
$this->formOutput['formConfig'][$data['groupIn']][$data['line']][] = $data;
// Passando aliases para os campos
$this->formOutput['validationAttributes']['formData.'.$data['identifier']] = $data['label'];
// Criando mapeamento entre identifiers e nomes no banco
$this->formOutput['formData'][$data['identifier']] = "";
$this->formOutput['identifierToField'][$data['identifier']] = $field;
}
return $this->formOutput;
}
}
?>
Esse arquivo ficou com a responsabilidade de compilar os dados para facilitar a manutenção. Sendo assim, eu apenas instancio ele nos meus FullPage Components do Livewire (FormComponent e ListComponent, nos casos genéricos) e passo para a tela os dados de output, assim a lógica de construção se mantém nos componentes Blade, deixando o código muito mais legível do que no Ipanel – uma abordagem que só pode acontecer por conta do Livewire e Laravel.
<?php
namespace App\Livewire;
use App\Controllers\GenericCtrl;
use Illuminate\Validation\ValidationException;
use Livewire\Attributes\Layout;
use Livewire\Component;
use App\Controllers\YamlInterpreter;
use App\Rules\ValidateCPF;
#[Layout('components.layouts.app')]
class FormComponent extends Component
{
public $rules = array();
public $validationAttributes = array();
public $messages = array();
public $formConfig = array();
public $formData = array();
public $selectsPopulate = array();
public $remoteUpdates = array();
public $isEdit = false;
public $identifierToField = array();
public $params = array();
protected function rules() {
return $this->rules;
}
protected function messages() {
return $this->messages;
}
public function mount($local, $id = null) {
$this->params = session('params');
$this->params['_local'] = $local;
$this->params['_id'] = $id;
$this->rules = array();
$this->validationAttributes = array();
$this->messages = array();
$this->formConfig = array();
$this->formData = array();
$this->identifierToField = array();
$this->renderUIViaYaml();
if(!is_null($id)) {
$this->isEdit = true;
$genericCtrl = new GenericCtrl($local);
$className = "App\\Models\\".$local;
$object = $genericCtrl->getObject($id);
if($object instanceof $className) {
$converted = [];
$objectArray = $object->toArray();
foreach ($this->identifierToField as $friendlyKey => $dbKey) {
$converted[$friendlyKey] = array_key_exists($dbKey, $objectArray) ? $objectArray[$dbKey] : null;
}
$this->formData = array_merge($this->formData, $converted);
}
foreach ($this->remoteUpdates as $identifier => $remoteConfig) {
if (!empty($this->formData[$identifier])) {
if (!empty($remoteConfig['customRemote'])) {
$customMethod = $remoteConfig['customRemote'];
$this->{$customMethod}();
} else {
$this->updateRemoteField($identifier, $remoteConfig);
}
}
}
}
}
public function renderUIViaYaml() {
$yamlInterpreter = new YamlInterpreter($this->params['_local']);
$formOutput = $yamlInterpreter->renderFormUIData();
$this->formConfig = $formOutput['formConfig'];
$this->selectsPopulate = $formOutput['selectsPopulate'];
$this->messages = $formOutput['messages'];
$this->rules = $formOutput['rules'];
$this->validationAttributes = $formOutput['validationAttributes'];
$this->formData = $formOutput['formData'];
$this->identifierToField = $formOutput['identifierToField'];
$this->remoteUpdates = $formOutput['remoteUpdates'];
}
public function updateRemoteField($parentIdentifier, $updateRemoteConfig) {
$genericCtrl = new GenericCtrl($this->params['_local']);
$remoteData = $genericCtrl->getRemoteData($this->formData[$parentIdentifier], $updateRemoteConfig);
$this->selectsPopulate[$updateRemoteConfig['remoteIdentifier']] = $remoteData;
}
public function submitForm() {
try {
$this->validate();
$formData = array();
$genericCtrl = new GenericCtrl($this->params['_local']);
foreach ($this->formData as $identifier => $value) {
$formData[$this->identifierToField[$identifier]] = $value;
}
if(!is_null($this->params['_id'])) {
$genericCtrl->update($this->params['_id'], $formData);
} else {
$genericCtrl->save($formData);
$this->reset('formData');
}
$this->dispatch('alert', icon: "success", title: "Sucesso!", position: "center");
$this->js("window.history.back()");
} catch (ValidationException $ex) {
$this->dispatch('alert', icon: "error", title: "Erro no Formulário", text: $ex->validator->errors()->first(), position: "center");
} catch (\Exception $ex) {
$this->dispatch('alert', icon: "error", title: "Erro Inesperado", text: $ex->getMessage(), position: "center");
}
}
public function render() {
return view('livewire.form-component');
}
}
?>
Route::get('{local}/List', ListComponent::class)->name("list.component");
Route::get('{local}/Form/{id?}', FormComponent::class)->name("form.component");
Route::get('{local}/UserForm/{id?}', UserForm::class)->name("user-form");
Aqui estão as rotas do sistema para que vocês possam ter uma noção melhor de como passo os dados para as views dinâmicas e para a view customizada UserForm. A publicação é extensa, mas demonstra a complexidade e as vantagens dessa abordagem.
Caso queira ver com mais detalhes o código e como eu fiz para gerar os campos usando o componente dynamic-form é só entrar no repo do github, acredito que os códigos estejam bem intuitivos mas caso tenha tido alguma dúvida sobre minha implementação ou de alguma coisa que não ficou claro fico a disposição para responder.
Qualquer dica de melhoria também é muito bem vinda.
O repositório no GitHub: https://github.com/Kzrtt/laravel_cms/tree/main
Aqui há alguns prints do sistema, caso queiram ver como ele está visualmente. Por enquanto ele está público, mas pode vir a ficar privado. Agradeço por terem tirado um tempo para ler o que escrevi.