Routing¶
O MIDDAG usa Symfony Routing com #[Route] attributes. Rotas são auto-discovered em controllers durante o boot() da extension.
Moodle padrao vs MIDDAG¶
| Aspecto | Moodle padrao | MIDDAG |
|---|---|---|
| Definicao de rota | URL hardcoded em new moodle_url('/local/plugin/page.php', [...]) |
#[Route(path: '/catalogo', name: 'catalogo_index')] |
| Parametros | required_param() / optional_param() manual |
Argumentos tipados no metodo (int $id) |
| Controller | Script PHP standalone (page.php) |
Metodo em classe controller com DI |
| Geracao de URL | new moodle_url(...) com path literal |
$this->url_generator('route_name', [...]) |
| Validacao de input | PARAM_INT, PARAM_TEXT por campo |
form_request declarativo auto-injetado |
| Resposta | echo $OUTPUT->header() + HTML + echo $OUTPUT->footer() |
return $this->render(...) ou return $this->json_response(...) |
| Discovery | Manual (arquivo por pagina) | Automatico via #[Route] attributes no boot() |
Moodle padrao (view.php):
require_once(__DIR__ . '/../../config.php');
require_login();
$id = required_param('id', PARAM_INT);
$PAGE->set_context(context_system::instance());
$PAGE->set_url(new moodle_url('/local/plugin/view.php', ['id' => $id]));
$PAGE->set_title('Catalogo');
echo $OUTPUT->header();
echo '<h1>Item ' . $id . '</h1>';
echo $OUTPUT->footer();
MIDDAG:
#[Route(path: '/catalogo/view/{id}', name: 'catalogo_view', methods: ['GET'])]
#[auth(login: true)]
public function view(int $id): Response {
$this->set_context(context::system());
$this->set_url_from_route('catalogo_view', ['id' => $id]);
$this->set_page_title('Catalogo');
return $this->render('<h1>Item ' . $id . '</h1>');
}
Criar uma rota¶
Anote o método do controller com #[Route]:
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\HttpFoundation\Response;
#[Route(path: '/catalogo', name: 'catalogo_index', methods: ['GET'])]
public function index(): Response
{
// ...
}
Parametros de rota são injetados como argumentos do método:
#[Route(path: '/catalogo/view/{id}', name: 'catalogo_view', methods: ['GET'])]
public function view(int $id): Response
{
// $id resolvido automaticamente a partir da URL.
}
Tres tipos de controller¶
1. Moodle page (base\controller)¶
Renderiza dentro do layout Moodle (header, footer, navegação). Auth declarada via #[auth] — handle() roda lazy no render().
namespace local_meuplugin\extensions\catalogo\controller;
use local_middag\base\controller;
use local_middag\facade\context;
use local_middag\framework\contract\attributes\auth;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
class catalogo_controller extends controller
{
#[Route(path: '/catalogo', name: 'catalogo_index', methods: ['GET'])]
#[auth(login: true)]
public function index(): Response
{
$this->set_context(context::system());
$this->set_url_from_route('catalogo_index');
$this->set_page_title('Catalogo');
$this->set_page_layout('standard');
return $this->render('<h1>Catalogo</h1>');
}
}
Métodos de renderizacao disponiveis:
| Metodo | Uso |
|---|---|
$this->render($html) |
HTML direto dentro do layout Moodle |
$this->render_from_template('plugin/template', $context) |
Mustache template |
$this->render_from_widget('ReactComponent', $props) |
Widget React |
$this->render_from_renderer($renderable, $component) |
Renderable Moodle |
2. API JSON (base\api_controller)¶
Forca respostas JSON com envelope padronizado { success, data, message }. Auth dual automática (wstoken → sessão Moodle) em todas as rotas. Erros de auth retornam JSON com o código HTTP correto — nunca redirect.
namespace local_meuplugin\extensions\catalogo\controller;
use local_middag\base\api_controller;
use local_middag\framework\contract\attributes\auth;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Routing\Attribute\Route;
class catalogo_api_controller extends api_controller
{
// Auth dual automática para todas as rotas por padrão.
#[Route(path: '/api/catalogo', name: 'api_catalogo_list', methods: ['GET'])]
public function list(): JsonResponse
{
return $this->json_response(['data' => ['item1', 'item2']]);
}
#[Route(path: '/api/catalogo', name: 'api_catalogo_store', methods: ['POST'])]
public function store(): JsonResponse
{
return $this->created(['data' => ['id' => 42]]);
}
// Rota pública — sem auth
#[Route(path: '/api/catalogo/ping', name: 'api_catalogo_ping', methods: ['GET'])]
#[auth(login: false)]
public function ping(): JsonResponse
{
return $this->json_response(['status' => 'ok']);
}
}
Helpers do api_controller:
| Metodo | Status HTTP |
|---|---|
$this->json_response($data) |
200 |
$this->created($data) |
201 |
$this->no_content() |
204 |
$this->not_found($msg) |
404 |
$this->forbidden($msg) |
403 |
$this->error_response($msg, $status) |
Customizado |
3. Inertia SPA (base\controller com Inertia)¶
Renderiza componentes React dentro do layout Moodle na primeira visita; retorna JSON puro em navegacoes SPA subsequentes.
#[Route(path: '/catalogo/dashboard', name: 'catalogo_dashboard', methods: ['GET'])]
#[auth(login: true)]
public function dashboard(): Response
{
$this->set_context(context::system());
$this->set_page_layout('standard');
return $this->inertia('Catalogo/Dashboard', [
'items' => $this->get_items(),
'stats' => $this->get_stats(),
]);
}
Autenticação com #[auth]¶
O atributo #[auth] declara os requisitos de auth da rota. O kernel o lê antes de executar a action — sem necessidade de chamadas imperativas dentro do método.
| Cenário | Declaração |
|---|---|
| Login obrigatório | #[auth(login: true)] |
| Login + capability | #[auth(login: true, capabilities: ['local/middag:manage'])] |
| Login + sesskey (POST com sessão) | #[auth(login: true, sesskey: true)] |
| Rota pública (sem auth) | #[auth(login: false)] |
| Default para todo o controller | #[auth(...)] na classe |
Prioridade: atributo no método > atributo na classe > sem auth.
Para api_controller com capabilities, faça override de pre_handle():
class meu_api_controller extends api_controller
{
public function pre_handle(): void
{
$this->set_require_capabilities(['local/middag:manage']);
parent::pre_handle(); // dual auth + handle()
}
}
Respostas de erro¶
| Controller | Falha | Resposta |
|---|---|---|
api_controller |
Token inválido | 401 { success: false, message: "...", error_code: 401 } |
api_controller |
Sem sessão / guest | 401 { success: false, message: "...", error_code: 401 } |
api_controller |
Sem capability | 403 { success: false, message: "...", error_code: 403 } |
controller (página) |
Sem login | Redirect do Moodle para página de login |
controller (página) |
Sem capability | Página de erro nativa do Moodle |
Cadeia de resolução de parametros¶
O framework resolve argumentos do controller nesta ordem:
graph LR
A["Route params
{id}, {slug}"] --> B["Request
Symfony Request"]
B --> C["form_request
Validação declarativa"]
C --> D["Container
Services via DI"]
D --> E["Inertia
Inertia adapter"]
Cada resolver tenta satisfazer o argumento. Se nenhum resolver, o framework lanca exceção.
Geração de URLs¶
Dentro de controllers¶
// Gerar URL a partir de rota nomeada.
$url = $this->url_generator('catalogo_view', ['id' => 42]);
// Redirecionar para rota nomeada.
return $this->redirect_to_route('catalogo_index');
Fora de controllers (via facade)¶
CSRF¶
| Contexto | Mecanismo |
|---|---|
| Session auth (Moodle pages, Inertia) | sesskey verificado automaticamente pelo require_sesskey() do Moodle |
| Token auth (API controllers) | Stateless, sem sesskey. Autenticação via token no header. |
Para endpoints POST com session auth, o Moodle exige sesskey no payload ou query string.
Entry points¶
Toda requisicao HTTP entra por um dos tres arquivos na raiz do plugin. Todos delegam para http_kernel.
| Arquivo | Uso |
|---|---|
index.php |
Requisicoes normais (pages, Inertia) |
webhook.php |
Callbacks externos (AJAX_SCRIPT, sem debug display) |
ajax.php |
Requisicoes AJAX internas (AJAX_SCRIPT, sem debug display) |
Os tres entry points chamam middag::handle(). O http_kernel resolve a rota, instancia o controller, executa resolvers e retorna a Response.