Pular para conteúdo

Hooks, Signals e Shortcodes

O que você vai construir

Neste tutorial você vai adicionar comportamento reativo a extension demo (construida nos tutoriais 01 e 02):

  • Action hook que dispara quando uma note e criada.
  • Subscriber que reage ao hook e registra log.
  • Filter que transforma o título da note (uppercase).
  • Shortcode que renderiza notes dentro de qualquer area de texto Moodle.

Ao final, a extension tera o ciclo reativo completo: publicar, reagir, transformar e exibir.

Tempo estimado: ~20 minutos.

Pre-requisitos

  • Tutoriais 01 e 02 concluidos (extension demo com CRUD de notes funcionando).

Passo 1: Publicar um action hook na criação

Edite classes/extensions/demo/note/note_service.php para disparar um hook quando uma note for criada.

Adicione o use no topo:

use local_middag\facade\hook_manager;

Altere o método create():

public function create(string $title, string $content): \local_middag\base\domain\item
{
    $dto = new item_dto(
        type: note::TYPE,
        fullname: $title,
        description: $content,
        status: 'published',
        metadata: [
            'title' => $title,
            'content' => $content,
        ],
    );

    $note = item_service::create($dto);

    // Dispara o action hook apos a criação.
    hook_manager::do_action('middag/demo/note_created', $note);

    return $note;
}

O que esta acontecendo

  • hook_manager::do_action() pública um hook nomeado. Qualquer subscriber registrado para middag/demo/note_created sera executado.
  • O hook e lateral: falhas em subscribers não afetam o fluxo principal (a note já foi criada com sucesso).
  • A facade hook_manager delega para \local_middag\framework\kernel\manager\hook_manager, que executa callbacks registrados por prioridade.

Convencao de nomes: Hooks do MIDDAG seguem o padrão middag/{extension}/{evento}. Isso evita colisao com hooks de outros plugins.


Passo 2: Reagir ao hook com um subscriber

Crie o arquivo:

classes/extensions/demo/hook/note_created_subscriber.php
<?php

declare(strict_types=1);

namespace local_middag\extensions\demo\hook;

defined('MOODLE_INTERNAL') || exit;

use local_middag\facade\hook_manager;

class note_created_subscriber
{
    /**
     * Registra o subscriber para o hook de criação de note.
     */
    public static function register(): void
    {
        hook_manager::add_action(
            'middag/demo/note_created',
            [self::class, 'handle'],
            10,  // prioridade
            1    // número de argumentos
        );
    }

    /**
     * Callback executado quando uma note e criada.
     *
     * @param \local_middag\base\domain\item $note A note recem-criada
     */
    public static function handle(\local_middag\base\domain\item $note): void
    {
        // Log via debugging do Moodle (visível com developer debug ativado).
        debugging(
            sprintf(
                '[demo] Note criada: ID=%d, título="%s"',
                $note->get_id(),
                $note->get_fullname()
            ),
            DEBUG_DEVELOPER
        );
    }
}

O que esta acontecendo

  • O arquivo reside em hook/ dentro da extension. O framework escaneia este diretorio automaticamente durante boot() e chama register() em cada classe encontrada.
  • hook_manager::add_action() registra o callback com prioridade 10 (default) e 1 argumento.
  • O método handle() recebe a note criada e registra uma mensagem de debug.

Nota: Em produção, você usaria o sistema de logging do framework (file_logger facade) em vez de debugging(). Para o tutorial, debugging() e mais simples de verificar.


Passo 3: Criar um filter para o título

Agora você vai criar um filter que transforma titulos de notes. Primeiro, publique o filter no service. Depois, registre um callback.

3.1 Publicar o filter no service

Edite note_service.php. Altere o método create() para aplicar o filter no título antes de criar a note:

public function create(string $title, string $content): \local_middag\base\domain\item
{
    // Aplica filters registrados no título.
    $title = hook_manager::apply_filters('middag/demo/note_title', $title, [
        'content' => $content,
    ]);

    $dto = new item_dto(
        type: note::TYPE,
        fullname: $title,
        description: $content,
        status: 'published',
        metadata: [
            'title' => $title,
            'content' => $content,
        ],
    );

    $note = item_service::create($dto);

    // Dispara o action hook apos a criação.
    hook_manager::do_action('middag/demo/note_created', $note);

    return $note;
}

3.2 Registrar o filter na extension

Edite classes/extensions/demo/demo_extension.php para registrar o filter durante boot():

<?php

declare(strict_types=1);

namespace local_middag\extensions\demo;

defined('MOODLE_INTERNAL') || exit;

use local_middag\base\extension;
use local_middag\extensions\demo\note\note_service;
use local_middag\facade\hook_manager;
use Psr\Container\ContainerInterface;

class demo_extension extends extension
{
    public const EXTENSION_IDNUMBER = 'demo';

    public function get_icon(): string
    {
        return 'app';
    }

    public function register(ContainerInterface $container): void
    {
        // Container registration (auto-wired para note_service).
    }

    public function boot(): void
    {
        parent::boot();

        // Registra um filter que converte titulos para uppercase.
        hook_manager::add_filter(
            'middag/demo/note_title',
            function (string $title): string {
                return mb_strtoupper($title);
            },
            10,  // prioridade
            1    // número de argumentos
        );
    }
}

O que esta acontecendo

  • hook_manager::apply_filters() passa o valor por todos os callbacks registrados e retorna o resultado transformado. Se nenhum filter estiver registrado, retorna o valor original.
  • Diferente de actions (laterais), filters propagam o valor de retorno em pipeline: cada callback recebe a saida do anterior.
  • O boot() chama parent::boot() primeiro para garantir que controllers e hooks do diretorio hook/ sejam registrados antes de adicionar lógica customizada.

Diferenca fundamental: Actions (do_action) executam side-effects sem valor de retorno. Filters (apply_filters) transformam um valor em cadeia sincrona.


Passo 4: Criar um shortcode

Adicione o registro do shortcode no método boot() da extension. Edite demo_extension.php:

use local_middag\facade\shortcode_manager;

Dentro do boot(), apos o registro do filter, adicione:

// Registra shortcode [middag type="demo_notes"].
shortcode_manager::register('demo_notes', [self::class, 'render_notes_shortcode']);

Adicione o método estatico na classe demo_extension:

/**
 * Renderiza o shortcode demo_notes.
 *
 * Uso: [middag type="demo_notes" limit="5"]
 *
 * @param array $attrs Atributos parseados do shortcode
 *
 * @return string HTML renderizado
 */
public static function render_notes_shortcode(array $attrs): string
{
    $limit = (int) ($attrs['limit'] ?? 5);

    $service = new \local_middag\extensions\demo\note\note_service();
    $result = $service->list_all();

    if ($result->is_empty()) {
        return '<p>Nenhuma note encontrada.</p>';
    }

    $html = '<div class="middag-demo-notes">';
    $html .= '<h4>Notas recentes</h4>';
    $html .= '<ul>';

    $count = 0;
    foreach ($result->items() as $note) {
        if ($count >= $limit) {
            break;
        }
        $html .= '<li><strong>' . s($note->get_fullname()) . '</strong></li>';
        $count++;
    }

    $html .= '</ul>';
    $html .= '</div>';

    return $html;
}

O que esta acontecendo

  • shortcode_manager::register() associa o tag demo_notes a um callback. A facade delega para \local_middag\framework\service\shortcode_manager.
  • O shortcode e invocado pela sintaxe [middag type="demo_notes"] em qualquer area de texto processada pelo MIDDAG.
  • Atributos adicionais (como limit) são parseados automaticamente e passados como array ao callback.
  • Por default, a saida do shortcode e sanitizada via clean_text(). Para output HTML sem sanitizacao, anote o método com #[trusted_output].

Passo 5: Usar o shortcode

Em qualquer area de texto do Moodle processada pelo MIDDAG (descricao de curso, bloco HTML, etc.), insira:

[middag type="demo_notes" limit="3"]

O framework substituira este tag pelo HTML renderizado: uma lista das 3 notes mais recentes.

Onde funciona: O shortcode e processado pelo filter filter_middag quando ativo no contexto do Moodle. Certifique-se de que o filtro de texto MIDDAG esta habilitado em Administracao do site > Plugins > Filtros de texto.


Passo 6: Testar

6.1 Limpar cache

php admin/cli/purge_caches.php

6.2 Testar o action hook (log)

  1. Ative o modo de debug do Moodle: Administracao do site > Desenvolvimento > Debugging > "DEVELOPER".
  2. Acesse /local/middag/index.php/demo/notes/create.
  3. Crie uma note com título "teste hooks" e conteúdo qualquer.
  4. Apos salvar, verifique as mensagens de debug. Você vera:
[demo] Note criada: ID=42, título="TESTE HOOKS"

Note que o título aparece em uppercase -- o filter foi aplicado antes da criação.

6.3 Testar o filter

Na listagem de notes (/local/middag/index.php/demo), todas as notes criadas apos a ativacao do filter terao o título em uppercase.

6.4 Testar o shortcode

  1. Crie uma página ou bloco HTML no Moodle.
  2. No conteúdo, insira: [middag type="demo_notes" limit="3"]
  3. Salve e visualize a página.
  4. O shortcode sera substituido pela lista de notes.

Resultado

Sua extension demo agora possui o ciclo reativo completo:

Mecanismo O que faz Onde esta
do_action() Publica evento lateral note_service::create()
add_action() Reage ao evento hook/note_created_subscriber.php
apply_filters() Transforma valor em pipeline note_service::create()
add_filter() Registra transformacao demo_extension::boot()
shortcode_manager::register() Registra shortcode demo_extension::boot()
[middag type="..."] Invoca shortcode em texto Qualquer area de texto Moodle

Estrutura final de arquivos

classes/extensions/demo/
    demo_extension.php
    note/
        note.php
        note_service.php
    controller/
        demo_controller.php
    hook/
        note_created_subscriber.php

Resumo do que você aprendeu

Conceito O que faz
hook_manager::do_action() Publica action hook (side-effect lateral)
hook_manager::add_action() Registra subscriber para action hook
hook_manager::apply_filters() Passa valor por pipeline de transformacao
hook_manager::add_filter() Registra callback de transformacao
shortcode_manager::register() Registra tag para renderizacao inline
[middag type="..."] Sintaxe de invocacao de shortcode
Diretorio hook/ Auto-discovered durante boot da extension

O que aprender a seguir

Com os fundamentos de extension, persistência e reatividade cobertos, explore os guides do framework:

  • Routing -- Rotas avancadas, middlewares, form requests e API controllers.
  • Commands e Jobs -- Execução sincrona e assincrona de trabalho.
  • Autorização -- Capabilities, policies e guards.
  • API Publica -- Contracts, facades e estabilidade.