Pular para conteúdo

Adicione Persistencia EAV

O que você vai construir

Neste tutorial você vai expandir a extension demo (criada no Tutorial 01) com:

  • Um item type demo:note declarado via attribute #[item_type].
  • Um service para criar e buscar notes.
  • Rotas de CRUD (listagem, criação e visualizacao).
  • Busca via query engine do framework.

Ao final, você podera criar notes pelo browser e ve-las listadas em uma página.

Tempo estimado: ~30 minutos.

Pre-requisitos

  • Tutorial 01 concluido (extension demo funcionando).
  • Familiaridade básica com o modelo EAV: items são armazenados em middag_items com metadata flexível em middag_itemmeta.

Passo 1: Declarar o item type

Crie o arquivo:

classes/extensions/demo/note/note.php
<?php

declare(strict_types=1);

namespace local_middag\extensions\demo\note;

defined('MOODLE_INTERNAL') || exit;

use local_middag\base\domain\item;
use local_middag\framework\contract\attributes\item_type;

#[item_type('demo:note', metadata_schema: ['title' => 'string', 'content' => 'string'], label: 'Demo Note')]
class note extends item
{
    public const TYPE = 'demo:note';

    public static function metadata_schema(): array
    {
        return [
            'title' => 'string',
            'content' => 'string',
        ];
    }
}

O que esta acontecendo

  • O attribute #[item_type('demo:note')] registra este tipo automaticamente no type_loader durante o boot. Não é necessário registro manual.
  • note estende \local_middag\base\domain\item, que é o aggregate central de persistência do framework.
  • metadata_schema() declara os campos EAV que serao armazenados em middag_itemmeta.
  • A constante TYPE define o tipo lógico utilizado em queries e no DTO.

EAV: O framework armazena campos basicos (fullname, description, status, etc.) na tabela middag_items. Campos adicionais (title, content) vao para middag_itemmeta como pares chave/valor.


Passo 2: Criar o service

Crie o arquivo:

classes/extensions/demo/note/note_service.php
<?php

declare(strict_types=1);

namespace local_middag\extensions\demo\note;

defined('MOODLE_INTERNAL') || exit;

use local_middag\base\domain\item_dto;
use local_middag\base\service;
use local_middag\facade\item_service;
use local_middag\facade\query_executor;
use local_middag\facade\query_factory;
use local_middag\framework\shared\enum\operator;

class note_service extends service
{
    /**
     * Cria uma nova note.
     *
     * @param string $title   Titulo da note
     * @param string $content Conteudo da note
     *
     * @return \local_middag\base\domain\item A note criada
     */
    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,
            ],
        );

        return item_service::create($dto);
    }

    /**
     * Busca uma note pelo ID.
     */
    public function find(int $id): ?\local_middag\base\domain\item
    {
        return item_service::find($id);
    }

    /**
     * Lista todas as notes usando o query engine.
     *
     * @return \local_middag\framework\infrastructure\query_engine\result
     */
    public function list_all(): \local_middag\framework\infrastructure\query_engine\result
    {
        $query = query_factory::new()
            ->where('type', operator::EQUAL, note::TYPE)
            ->order_by('timecreated DESC')
            ->with_metadata(['title', 'content']);

        return query_executor::execute($query);
    }
}

O que esta acontecendo

  • note_service estende \local_middag\base\service, a classe-base para services de aplicação.
  • create() monta um item_dto e delega a criação para a facade item_service::create().
  • list_all() usa o query engine: query_factory::new() cria um query_builder, e query_executor::execute() retorna um objeto result iteravel.
  • O query_builder e imutavel: cada método retorna uma nova instancia.

Passo 3: Registrar o service no container

Edite classes/extensions/demo/demo_extension.php para registrar o service na fase register():

<?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 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
    {
        // Registra o note_service como singleton no container.
        // Em runtime, pode ser resolvido via $container->get(note_service::class).
    }
}

Nota: O container do MIDDAG resolve classes concretas automaticamente por convenção quando o sufixo _service e usado (ADR-601). Para este tutorial, o registro explícito via register() é opcional -- o auto-wiring já resolve note_service se ele não tiver dependências externas. O método esta aqui para ilustrar o ciclo de vida.


Passo 4: Criar os controllers CRUD

Edite classes/extensions/demo/controller/demo_controller.php para adicionar as rotas de CRUD:

<?php

declare(strict_types=1);

namespace local_middag\extensions\demo\controller;

defined('MOODLE_INTERNAL') || exit;

use html_writer;
use local_middag\base\controller;
use local_middag\extensions\demo\note\note_service;
use local_middag\facade\context;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;

class demo_controller extends controller
{
    #[Route(path: '/demo', name: 'demo_index', methods: ['GET'])]
    public function index(): Response
    {
        $this->set_require_login();
        $this->set_context(context::system());
        $this->set_url_from_route('demo_index');
        $this->set_page_title('Demo Notes');
        $this->set_page_heading('Demo Notes');
        $this->set_page_layout('standard');

        $service = new note_service();
        $result = $service->list_all();

        $output = '<h2>Notes</h2>';
        $output .= html_writer::link(
            $this->url_generator('demo_notes_create')->out(false),
            'Criar nova note',
            ['class' => 'btn btn-primary mb-3']
        );

        if ($result->is_empty()) {
            $output .= '<p>Nenhuma note encontrada. Crie a primeira!</p>';
        } else {
            $output .= '<table class="table table-striped">';
            $output .= '<thead><tr><th>ID</th><th>Titulo</th><th>Status</th><th>Acoes</th></tr></thead>';
            $output .= '<tbody>';

            foreach ($result->items() as $note) {
                $view_url = $this->url_generator('demo_notes_view', ['id' => $note->get_id()])->out(false);
                $output .= '<tr>';
                $output .= '<td>' . $note->get_id() . '</td>';
                $output .= '<td>' . s($note->get_fullname()) . '</td>';
                $output .= '<td>' . s($note->get_status()) . '</td>';
                $output .= '<td>' . html_writer::link($view_url, 'Ver') . '</td>';
                $output .= '</tr>';
            }

            $output .= '</tbody></table>';
        }

        return $this->render($output);
    }

    #[Route(path: '/demo/notes/create', name: 'demo_notes_create', methods: ['GET', 'POST'])]
    public function create(): Response
    {
        $this->set_require_login();
        $this->set_context(context::system());
        $this->set_url_from_route('demo_notes_create');
        $this->set_page_title('Criar Note');
        $this->set_page_heading('Criar Note');
        $this->set_page_layout('standard');

        // Processar POST.
        if ($this->request->getMethod() === 'POST') {
            $title = $this->payload['title'] ?? '';
            $content = $this->payload['content'] ?? '';

            if ($title !== '' && $content !== '') {
                $service = new note_service();
                $service->create($title, $content);

                return $this->redirect_to_route('demo_index');
            }
        }

        // Formulario simples.
        $action_url = $this->url_generator('demo_notes_create')->out(false);
        $sesskey = sesskey();

        $output = <<<HTML
        <form method="post" action="{$action_url}">
            <input type="hidden" name="sesskey" value="{$sesskey}">
            <div class="form-group mb-3">
                <label for="title">Titulo</label>
                <input type="text" name="title" id="title" class="form-control" required>
            </div>
            <div class="form-group mb-3">
                <label for="content">Conteudo</label>
                <textarea name="content" id="content" class="form-control" rows="5" required></textarea>
            </div>
            <button type="submit" class="btn btn-primary">Salvar</button>
            <a href="{$this->url_generator('demo_index')->out(false)}" class="btn btn-secondary">Cancelar</a>
        </form>
        HTML;

        return $this->render($output);
    }

    #[Route(path: '/demo/notes/{id}', name: 'demo_notes_view', methods: ['GET'])]
    public function view(int $id): Response
    {
        $this->set_require_login();
        $this->set_context(context::system());
        $this->set_url_from_route('demo_notes_view', ['id' => $id]);
        $this->set_page_title('Visualizar Note');
        $this->set_page_heading('Visualizar Note');
        $this->set_page_layout('standard');

        $service = new note_service();
        $note = $service->find($id);

        if ($note === null) {
            return $this->error_page('Note não encontrada.', 404);
        }

        $meta = $note->get_metadata();
        $title = s($meta['title'] ?? $note->get_fullname());
        $content = format_text($meta['content'] ?? $note->get_description() ?? '', FORMAT_PLAIN);
        $back_url = $this->url_generator('demo_index')->out(false);

        $output = <<<HTML
        <div class="card">
            <div class="card-body">
                <h3>{$title}</h3>
                <div class="mt-3">{$content}</div>
                <hr>
                <small class="text-muted">
                    ID: {$note->get_id()} |
                    Status: {$note->get_status()} |
                    Criado em: {$note->get_timecreated()}
                </small>
            </div>
        </div>
        <a href="{$back_url}" class="btn btn-secondary mt-3">Voltar</a>
        HTML;

        return $this->render($output);
    }
}

Passo 5: Testar

5.1 Limpar cache

php admin/cli/purge_caches.php

5.2 Listar notes (vazio)

Acesse no browser:

https://seu-moodle.local/local/middag/index.php/demo

Você vera a página de listagem com a mensagem "Nenhuma note encontrada. Crie a primeira!" é o botao "Criar nova note".

5.3 Criar uma note

  1. Clique em Criar nova note.
  2. Preencha o título ("Minha primeira note") e conteúdo ("Conteudo de teste").
  3. Clique em Salvar.

Você sera redirecionado para a listagem, que agora mostra a note criada.

5.4 Visualizar uma note

Clique em Ver na listagem. A página de detalhe mostra título, conteúdo, ID, status e timestamp de criação.


Resultado

Você agora tem um CRUD funcional com:

  • Persistencia EAV via middag_items + middag_itemmeta.
  • Item type registrado automaticamente por #[item_type].
  • Query engine para queries tipadas via query_builder.
  • Facades (item_service, query_factory, query_executor) como API estavel.

Estrutura final de arquivos

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

Resumo do que você aprendeu

Conceito O que faz
#[item_type] Registra um tipo de item automaticamente no framework
base\domain\item Aggregate central EAV do framework
base\domain\item_dto DTO para criar/atualizar items
item_service facade CRUD de items via API estavel
query_factory facade Cria queries imutaveis com query_builder
query_executor facade Executa queries e retorna result iteravel
metadata_schema() Declara campos EAV flexiveis por tipo

Proximo passo

No Tutorial 03 -- Hooks, Signals e Shortcodes, você vai adicionar comportamento reativo: publicar hooks quando notes são criadas, transformar titulos via filters e renderizar notes via shortcodes.