Executando verificação de segurança...
3
kurt
8 min de leitura ·

[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>
            &nbsp;<span class="font-semibold">Cancelar</span>&nbsp;
          </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>
            &nbsp;<span class="font-semibold">Salvar</span>&nbsp;
          </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.

Carregando publicação patrocinada...