Pular para conteúdo

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.

use local_middag\framework\contract\attributes\auth;
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)

use local_middag\middag;

$url = middag::url_generator('catalogo_view', ['id' => 42]);

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)
// index.php
use local_middag\middag;

require __DIR__ . '/../../config.php';

middag::handle();

Os tres entry points chamam middag::handle(). O http_kernel resolve a rota, instancia o controller, executa resolvers e retorna a Response.