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:notedeclarado 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_itemscom metadata flexível emmiddag_itemmeta.
Passo 1: Declarar o item type¶
Crie o arquivo:
<?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 notype_loaderdurante o boot. Não é necessário registro manual. noteestende\local_middag\base\domain\item, que é o aggregate central de persistência do framework.metadata_schema()declara os campos EAV que serao armazenados emmiddag_itemmeta.- A constante
TYPEdefine 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 paramiddag_itemmetacomo pares chave/valor.
Passo 2: Criar o service¶
Crie o arquivo:
<?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_serviceestende\local_middag\base\service, a classe-base para services de aplicação.create()monta umitem_dtoe delega a criação para a facadeitem_service::create().list_all()usa o query engine:query_factory::new()cria umquery_builder, equery_executor::execute()retorna um objetoresultiteravel.- O
query_buildere 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
_servicee usado (ADR-601). Para este tutorial, o registro explícito viaregister()é opcional -- o auto-wiring já resolvenote_servicese 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¶
5.2 Listar notes (vazio)¶
Acesse no browser:
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¶
- Clique em Criar nova note.
- Preencha o título ("Minha primeira note") e conteúdo ("Conteudo de teste").
- 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.