Pular para conteúdo

Formulários

A DSL de formulários do MIDDAG permite declarar campos, validação e condicionais em uma única classe PHP. O framework renderiza o mesmo schema via moodleform (Moodle server-side) ou Inertia/React — sem reescrever a definição.


Moodle padrao vs MIDDAG

Aspecto Moodle padrao (moodleform) MIDDAG (form DSL)
Definicao de campos $mform->addElement('text', ...) imperativo field::text('name')->required() declarativo
Validacao validation() com $errors[] manual Inline na DSL (->min(), ->max(), ->pattern()) ou form_request
Condicionais $mform->hideIf() / $mform->disabledIf() ->visible_when(), ->disabled_when() com operadores tipados
Layout $mform->addElement('header', ...) section::of() e group::of() composiveis
Renderizacao Acoplado a HTML server-side Neutro — mform ou Inertia/React com o mesmo schema
Injecao no controller Manual (new form(...) + $form->get_data()) Automatica via type-hint (contact_form $form)

Moodle padrao:

class contact_form extends moodleform {
    protected function definition() {
        $mform = $this->_form;
        $mform->addElement('text', 'name', get_string('name'));
        $mform->setType('name', PARAM_TEXT);
        $mform->addRule('name', null, 'required');
        $mform->addRule('name', null, 'maxlength', 255);

        $mform->addElement('text', 'email', get_string('email'));
        $mform->setType('email', PARAM_EMAIL);
        $mform->addRule('email', null, 'required');
    }
}

MIDDAG:

final class contact_form extends form {
    public function schema(): array {
        return [
            field::text('name')->label('contact.name')->required()->max(255),
            field::email('email')->label('contact.email')->required(),
        ];
    }
}

Exemplo minimo

namespace local_meuplugin\extensions\catalogo\site;

use local_middag\base\form;
use local_middag\base\form\field;

final class contact_form extends form
{
    public function schema(): array
    {
        return [
            field::text('name')
                ->label('contact.name')
                ->required()
                ->max(255),

            field::email('email')
                ->label('contact.email')
                ->required(),
        ];
    }
}

Uso no controller

O form_resolver hidrata e valida automaticamente antes do controller executar.

use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\HttpFoundation\Response;

#[Route(path: '/contatos/criar', name: 'contacts.create', methods: ['GET', 'POST'])]
public function create(contact_form $form): Response
{
    if ($form->is_submitted_and_valid()) {
        $this->service->save($form->validated());
        return $this->redirect_to_route('contacts.index');
    }

    return $this->render_form($form);
}

Catálogo de campos

Construtor Atributos extras
field::text(name) ->max(int), ->min(int), ->pattern(regex)
field::textarea(name) ->max(int), ->rows(int), ->cols(int)
field::password(name) ->max(int), ->min(int), ->pattern(regex)
field::email(name) ->max(int), ->pattern(regex)
field::url(name) ->max(int), ->pattern(regex)
field::int(name) ->max(num), ->min(num), ->step(num)
field::float(name) ->max(num), ->min(num), ->step(num)
field::select(name) ->options(array), ->options_from(callable), ->searchable(bool)
field::multiselect(name) ->options(array), ->options_from(callable), ->searchable(bool)
field::radio(name) ->options(array), ->options_from(callable)
field::checkbox(name)
field::switch(name)
field::date(name) ->min_date($date), ->max_date($date), ->optional(bool)
field::datetime(name) ->min_date($date), ->max_date($date), ->optional(bool)
field::duration(name) ->units(array), ->default_unit(int)
field::file(name) ->accept(array), ->max_size(int), ->context_id(int)
field::entity_picker(name) ->source(string), ->display_field(string), ->value_field(string)
field::hidden(name)
field::static(name) ->content($key, $component?)
field::header(name) ->content($key, $component?)

Todos os campos aceitam os atributos comuns: ->label(), ->help(), ->placeholder(), ->default(), ->required(), ->readonly(), ->rule(), ->meta() e os condicionais descritos abaixo.

Catálogo fechado — adicionar um tipo novo é decisão arquitetural (ADR-806).


Layout

Agrupe campos com section (com header) ou group (inline, multi-coluna):

use local_middag\base\form\field;
use local_middag\base\form\section;
use local_middag\base\form\group;

public function schema(): array
{
    return [
        section::of('basic')
            ->label('form.section.basic')
            ->fields([
                field::text('name')->label('site.name')->required(),
                field::url('url')->label('site.url')->required(),
            ]),

        section::of('config')
            ->label('form.section.config')
            ->fields([
                group::of('credentials')->fields([
                    field::text('api_user')->label('site.api_user'),
                    field::password('api_key')->label('site.api_key'),
                ]),
            ]),
    ];
}

Condicionais

Condicionais tornam campos reativos sem código JavaScript manual:

field::password('api_key')
    ->label('site.api_key')
    ->visible_when('provider', 'in', ['eduzz', 'hotmart'])
    ->required_when('provider', 'in', ['eduzz', 'hotmart']),

field::select('plan')
    ->label('site.plan')
    ->disabled_when('status', 'eq', 'inactive'),
Método Semântica
->visible_when() Exibe o campo quando a condição é verdadeira
->hidden_when() Inverso de visible_when
->required_when() Torna o campo obrigatório condicionalmente
->disabled_when() Desativa o input condicionalmente

Operadores disponíveis: eq, neq, in, not_in, gt, gte, lt, lte, truthy, falsy, exists, empty, matches.

Operadores matches, gt/gte/lt/lte em campos string e condições compostas não são suportados pelo renderer mform — geram aviso em runtime e degradam para validação server-side pós-submit.


Validação

Inline (caso comum)

field::text('code')
    ->required()
    ->min(3)
    ->max(50)
    ->pattern('/^[A-Z]{2}-\d+$/'),

field::select('type')
    ->options(['article' => 'type.article', 'page' => 'type.page'])
    ->required(),

Escalation para form_request

Quando a validação exige regras cross-field, consulta ao banco ou compartilhamento com endpoint REST, declare REQUEST:

final class site_form extends form
{
    public const REQUEST = site_store_request::class;

    public function schema(): array
    {
        return [ /* estrutura/UI apenas */ ];
    }
}

A form_request continua sendo @api independente e continua funcionando em endpoints REST sem UI (ADR-802).


Internacionalização

Labels, help text, placeholders e options de select usam chaves lang_string. O componente é derivado automaticamente do namespace da extension:

field::text('name')->label('site.name')              // resolve no component da extension
field::text('name')->label('common.name', 'core')    // override de component

Options de select:

field::select('provider')
    ->options([
        'eduzz'       => 'provider.eduzz',
        'woocommerce' => 'provider.woocommerce',
    ])

Para options vindas do banco, use ->options_from(callable):

field::select('roleid')
    ->options_from(fn() => role_support::get_role_options())

Strings raw (sem resolução via lang_string) são proibidas — PHPStan custom rule detecta em tempo de análise (ADR-904).


Renderização

O controller escolhe o renderer por chamada, não por configuração global:

// Default — mform (Moodle server-side HTML)
return $this->render_form($form);

// Override pontual para Inertia/React
return $this->render_form($form, target: render_target::INERTIA);

O mesmo schema() alimenta ambos os renderers — sem reescrita.


Migração de moodleform

Para migrar um moodleform existente:

  1. Crie a classe {entity}_form em extensions/{slug}/{aggregate}/
  2. Mova os campos de definition() para schema() usando a DSL
  3. Remova regras de validação inline do moodleform — declare na DSL ou em form_request
  4. Atualize o controller para receber o form via type-hint e usar render_form()
  5. Marque o moodleform original com @deprecated since 5.x — remova quando o form for tocado novamente

Referência de PoC: product_edit_form (ADR-802).