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):
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:
- Crie a classe
{entity}_formemextensions/{slug}/{aggregate}/ - Mova os campos de
definition()paraschema()usando a DSL - Remova regras de validação inline do
moodleform— declare na DSL ou emform_request - Atualize o controller para receber o form via type-hint e usar
render_form() - Marque o
moodleformoriginal com@deprecated since 5.x— remova quando o form for tocado novamente
Referência de PoC: product_edit_form (ADR-802).