M

Manual Rede das Artes

Documentação para Desenvolvedores

Guia completo para desenvolvimento na plataforma Mapas Culturais

Mapas Culturais - Documentação para Desenvolvedores

Plataforma de mapeamento cultural (agentes, espaços, eventos, projetos). Instalação RedeMapas do framework open-source Mapas Culturais (PHP 8.3 + Slim 4 + Doctrine ORM + PostgreSQL/PostGIS).


Setup local (< 10 min)

Pré-requisitos

FerramentaVersão mínima
Docker + Compose24+
Node.js + pnpm20+ / 8+
PHP + Composer8.3+ (só para IDE autocomplete)

Passos

# 1. Clone com submodules
git clone --recurse-submodules <repo-url>
cd mapas

# 2. Variáveis de ambiente (copie e edite)
cp .env.example .env          # se existir, senão crie baseado no compose.yaml

# 3. Suba o stack
docker compose up -d

# app em  http://localhost:8080
# HTTPS   https://mapas.localhost:8443  (requer confiar no CA do Caddy — veja abaixo)
# Mailpit http://localhost:8025

# 4. Dependências JS (dentro do container)
docker compose exec mapas bash -c "cd src && pnpm install && pnpm run build"

HTTPS local (necessário para PWA/ServiceWorker)

# Instalar o CA do Caddy no sistema (CachyOS/Arch)
docker compose exec caddy cat /data/caddy/pki/authorities/local/root.crt > /tmp/caddy-root.crt
sudo trust anchor --store /tmp/caddy-root.crt && sudo update-ca-trust

# Instalar no Chrome/Chromium
mkdir -p $HOME/.pki/nssdb
certutil -d sql:$HOME/.pki/nssdb -N --empty-password 2>/dev/null || true
certutil -d sql:$HOME/.pki/nssdb -A -t "CT,," -n "Caddy Local CA" -i /tmp/caddy-root.crt
# Reinicie o Chrome

Verificação

  • http://localhost:8080 carrega a home
  • docker compose exec mapas ./vendor/bin/phpunit tests/ passa (verde)

Estrutura do projeto

mapas/
├── config/             # Config PHP por feature (db, mailer, maps, etc.)
├── dev/                # Scripts de desenvolvimento (bash.sh, shell.sh, psql.sh)
├── scripts/            # Scripts utilitários (execute-job, db-update, run-tests)
├── src/
│   ├── core/           # Framework base (App, Entity, Controller, Hooks, Theme…)
│   ├── modules/        # Features modulares (Opportunities, Home, GeoDivisions…)
│   ├── plugins/        # Plugins opcionais (SpamDetector, Metabase, MapasBlame…)
│   ├── themes/
│   │   ├── BaseV1/     # Tema legado
│   │   ├── BaseV2/     # Tema moderno (base)
│   │   └── RedeMapas/  # ← tema ativo (submodule git separado)
│   └── db-updates.php  # Migrações (auto-aplicadas no start do container)
├── tests/              # PHPUnit (necessita DB — roda dentro do container)
├── compose.yaml        # Stack principal
└── compose.override.yml# Caddy HTTPS local

Tema ativo: RedeMapas

src/themes/RedeMapas/ é um submodule git separado. Commits nele precisam de dois passos:

cd src/themes/RedeMapas
git add <arquivo> && git commit -m "fix: ..."

# Depois, no repo pai:
cd ../../..
git add src/themes/RedeMapas && git commit -m "chore: bump RedeMapas submodule"

Estrutura do tema:

RedeMapas/
├── Theme.php           # _init(): hooks, controllers, assets, metadata
├── assets-src/
│   ├── js/             # Fontes JS (esbuild)
│   └── sass/           # Fontes CSS (dart-sass)
├── assets/             # Build output (.gitignored no submodule)
├── layouts/            # Templates PHP de layout
├── views/              # Templates PHP de views
├── Controllers/        # Controllers HTTP do tema (ex: Push)
├── Jobs/               # JobTypes assíncronos (ex: SendWebPushNotification)
├── Push/               # Infraestrutura Web Push (SubscriptionStore, PushConfigBuilder)
└── Pwa/                # PWA (WebmanifestBuilder, HeadTagsBuilder)

Conceitos core

App singleton

$app = \MapasCulturais\App::i();
$app->em;          // Doctrine EntityManager
$app->user;        // usuário logado (GuestUser se anônimo)
$app->config[...]; // configurações
$app->log;         // Monolog logger

Entidades

Doctrine PHP 8 attributes. Todas estendem src/core/Entity.php. Status constants: STATUS_ENABLED=1, STATUS_DRAFT=0, STATUS_DISABLED=-9, STATUS_TRASH=-10, STATUS_ARCHIVED=-2.

Entidades principais: Agent, Space, Event, Project, Opportunity, Registration, User, Notification.

Hooks

$app->hook('entity(Agent).save:before', function() {
    // $this = instância da entidade
});

$app->hook('view.render(site/index):before', function() {
    // roda antes de renderizar a view site/index
});

$app->hook('mapas.printJsObject:before', function() {
    $this->jsObject['minhaConfig'] = [...]; // injeta no objeto JS global `Mapas`
});

Controllers

// Registrar
$app->registerController('meu', MeuController::class);

// Métodos = verbos HTTP + ação
// GET  /meu/listar/    → GET_listar()
// POST /meu/salvar/    → POST_salvar()
class MeuController extends Controller {
    public function GET_listar(): void {
        $this->json(['items' => [...]]);
    }
}

Jobs assíncronos

# Processar jobs pendentes
docker compose exec mapas ./scripts/execute-job.sh

Jobs são enfileirados via hooks (ex: entity(Notification).insert:after) e precisam ser processados manualmente em dev ou via cron em produção.

Metadata de entidades

Metadata precisa ser registrado antes de usar:

$app->registerMetadata(
    new \MapasCulturais\Definitions\Metadata('minhaChave', ['label' => 'Minha chave', 'private' => true]),
    \MapasCulturais\Entities\User::class
);

// Leitura/escrita
$user->getMetadata('minhaChave');
$user->setMetadata('minhaChave', json_encode($valor)); // arrays: sempre json_encode
$user->save(true);

Tarefas comuns

Build de assets (frontend)

# Dentro do container
docker compose exec mapas bash
cd src
pnpm run build    # produção
pnpm run dev      # dev com source maps
pnpm run watch    # watch mode

Assets do tema RedeMapas ficam em src/themes/RedeMapas/assets-src/ (fontes) e src/themes/RedeMapas/assets/ (output — não comitar no repo pai).

Rodar testes

# Todos os testes
docker compose exec mapas ./vendor/bin/phpunit tests/

# Arquivo específico
docker compose exec mapas ./vendor/bin/phpunit tests/src/EntitiesTest.php

# Método específico
docker compose exec mapas ./vendor/bin/phpunit tests/src/EntitiesTest.php --filter testAgentCreation

Cada teste usa transação com rollback automático em tearDown(). Base: tests/src/Abstract/TestCase.php.

Shell interativo PHP (PsySH)

docker compose exec mapas ./scripts/shell.sh
# ou
docker compose exec mapas ./dev/shell.sh

Útil para testar código, inspecionar entidades, disparar jobs manualmente:

// No prompt PsySH:
$app->disableAccessControl();
$user = $app->repo('User')->find(1);
// ... manipular entidades
$app->enableAccessControl();

Migrações de banco

# Aplicar migrações pendentes
docker compose exec mapas ./scripts/db-update.sh

# Conectar ao PostgreSQL
docker compose exec mapas ./dev/psql.sh
# ou direto
docker compose exec postgres psql -U mapas -d mapas

Migrações ficam em src/db-updates.php e são aplicadas automaticamente no start do container.

Adicionar uma nova rota / controller

  1. Criar src/themes/RedeMapas/Controllers/MeuController.php estendendo Controller

  2. Registrar em Theme.php → método _init():

    $app->registerController('meu', MeuController::class);
  3. Acessar em https://mapas.localhost:8443/meu/acao/

Injetar variáveis no JS global (Mapas.*)

// Em Theme.php, hook mapas.printJsObject:before:
$app->hook('mapas.printJsObject:before', function() {
    $this->jsObject['minhaConfig'] = ['key' => 'value'];
});

// No JS:
var config = window.Mapas?.minhaConfig;

Atenção: o layout da home (layouts/home.php) precisa chamar <?php $this->printJsObject(); ?> para que o objeto Mapas exista.


Debugging

Logs de erros

# PHP errors
docker compose exec mapas tail -f var/logs/error.log

# Logs do container
docker compose logs mapas --tail=50 -f

# Habilitar debug verbose
# Em dev/config.d/0.main.php:
'app.mode' => 'development',
# E definir APP_DEBUG=true no .env

Erros comuns

The 'Entity::chave' metadata is not registered → Falta $app->registerMetadata(...) em Theme.php ou no Module antes de chamar setMetadata.

PermissionDenied: O usuário 0 não tem permissão → No shell: use $app->disableAccessControl() antes e $app->enableAccessControl() depois.

Call to undefined method App::authenticateWithUserEntity() → Não existe esse método. Use $app->disableAccessControl() para operações CLI.

Service Worker preso em "trying to install" → SSL do Caddy não está confiado. Siga os passos de HTTPS local acima.

500 em endpoint do tema → Cheque var/logs/error.log. Se vazio, ative display_errors via env ou inspecione via PsySH.

Queries SQL úteis

# Conectar
docker compose exec mapas ./dev/psql.sh

# Ver metadata de um usuário
SELECT * FROM user_metadata WHERE object_id = 1;

# Jobs pendentes
SELECT * FROM job WHERE status = 0 ORDER BY create_timestamp DESC LIMIT 10;

# Notificações de um usuário
SELECT * FROM notification WHERE user_id = 1 ORDER BY create_timestamp DESC LIMIT 10;

Frontend — arquitetura do core

Visão geral

O frontend é dividido em duas camadas que coexistem:

CamadaTecnologiaOnde vive
Componentes reativosVue 3 + Piniasrc/modules/Components/
CSS global / design systemSCSS (ITCSS)src/themes/BaseV2/assets-src/sass/
Build toolingesbuild + dart-sasssrc/node_scripts/
Páginas simples / PWAVanilla JSsrc/themes/RedeMapas/assets-src/js/

Não há Alpine.js, Next.js ou SSR de JavaScript. O PHP renderiza o HTML, e o Vue 3 monta em #main-app para partes interativas.


Objeto global Mapas

window.Mapas é o contrato entre PHP e JS. É populado pelo hook mapas.printJsObject:before e serializado como <script>var Mapas = {...};</script> no rodapé de cada página (via printJsObject()).

Estrutura relevante:

Mapas = {
  baseURL: 'https://mapas.localhost:8443/',
  assetURL: '...',
  userId: 42,               // null se não logado
  user: { id, name, ... },
  config: {
    locale: 'pt-BR',
    timezone: 'America/Sao_Paulo',
    currency: 'BRL',
    iconset: { 'chevron-right': 'ph:chevron-right', ... },
  },
  request: { controller, action, urlData, id },
  routes: { ... },          // mapa de rotas registradas
  EntitiesDescription: { agent, space, event, project, ... },
  Taxonomies: { ... },
  // injetado pelo RedeMapas:
  redemapasPush: { enabled, publicKey, subscribeUrl, serviceWorkerUrl, strings },
}

Como injetar dados do PHP:

// Em Theme.php ou Module.php — hook mapas.printJsObject:before
$app->hook('mapas.printJsObject:before', function() {
    $this->jsObject['minhaConfig'] = ['key' => 'value'];
});
// No JS:
const config = window.Mapas?.minhaConfig;

Atenção: o layout home (layouts/home.php) não chama printJsObject() automaticamente. Sempre garantir que o layout do template chame <?php $this->printJsObject(); ?> antes dos scripts.


Sistema de componentes Vue 3

Onde vivem: src/modules/Components/components/<nome-do-componente>/

Cada componente tem até 4 arquivos:

ArquivoPapel
template.phpMarkup PHP — renderizado para string e enviado ao browser como JSON
script.jsDefinição do componente Vue 3 (Options API ou Composition API)
init.phpInicialização PHP opcional (roda antes do printJsObject)
texts.phpStrings i18n do componente

Fluxo de registro:

PHP importa componente → template renderizado → serializado em $TEMPLATES (JSON)

Browser recebe window.$TEMPLATES = { 'mc-icon': '<svg>...</svg>', ... }

vue-init.js registra globalmente: app.component('mc-icon', { template: $TEMPLATES['mc-icon'], ...script.js })

Vue monta em #main-app

Importar um componente em uma view PHP:

<?php $this->import('mc-icon'); ?>
<mc-icon name="ph:star" />

<?php $this->import('entity-card'); ?>
<entity-card :entity="<?= json_encode($entity->simplify('id,name,type')) ?>"></entity-card>

Componentes disponíveis (50+): mc-icon, mc-modal, mc-tabs, mc-loading, mc-datepicker, mc-map, entity-card, space-card, agent-card, mc-multiselect, mc-draggable, entre outros.


vue-init.js — bootstrap do Vue

Localização compilada: src/modules/Components/assets/js/vue-init.js

O que ele faz:

// Cria a app Vue 3
const app = createApp({ ... })

// Plugins registrados:
app.use(pinia)           // Pinia — gerenciamento de estado global
app.use(iconify)         // Iconify — ícones (via Mapas.config.iconset)
app.use(mediaQuery)      // $media() — breakpoints reativos

// Stores Pinia globalmente:
useEntitiesCache()       // cache de entidades buscadas via API
useEntitiesLists()       // listas paginadas
useGlobalState()         // estado geral ($MAPAS, $global)

// Propriedades globais:
app.config.globalProperties.$MAPAS = Mapas
app.config.globalProperties.$DESCRIPTIONS = Mapas.EntitiesDescription
app.config.globalProperties.$TAXONOMIES = Mapas.Taxonomies

// Monta em:
app.mount('#main-app')

Acessar o estado Pinia fora do Vue:

import { useEntitiesCache } from './components-base/global-state.js'
const cache = useEntitiesCache()
cache.fetchEntity('agent', 42)

API JS de entidades

src/modules/Components/assets/js/components-base/API.js encapsula as chamadas à API REST:

// Buscar entidade
const agent = await API.getEntity('agent', 42)

// Buscar lista com filtros
const agents = await API.find('agent', {
  'name': 'LIKE%fulano%',
  '@select': 'id,name,type',
  '@limit': 10,
})

// Buscar contagem
const total = await API.find('agent', { '@count': 1 })

// Salvar / atualizar
await API.save('agent', { id: 42, name: 'Novo nome' })

A API usa o endpoint /api/{entidade}/find do Mapas Culturais. Respostas são cacheadas no Pinia por padrão.

Parâmetros especiais de query:

ParâmetroEfeito
@selectCampos a retornar (id,name,type)
@limitLimite de resultados
@offsetPaginação
@count=1Retorna inteiro com total
@orderOrdenação (name ASC)
status=1Filtro por status

SCSS — design system (ITCSS)

O CSS segue a metodologia ITCSS (Inverted Triangle CSS):

src/themes/BaseV2/assets-src/sass/
├── 0.settings/
│   ├── _variables.scss    ← design tokens: cores, fontes, espaçamentos
│   ├── _mixins.scss       ← @mixin desktop/mobile, size(), sr-only
│   ├── _typography.scss   ← @font-face Open Sans
│   └── _global.scss       ← reset global, h1-h6, a, p
├── 1.objects/             ← padrões reutilizáveis sem estilo visual
├── 2.components/          ← 180+ componentes UI
├── layouts/               ← header, footer, entity layout, tabs
└── pages/                 ← estilos específicos por página/entidade

Design tokens principais (_variables.scss):

// Cores de entidades
$color-agents:       #EF7B45;
$color-events:       #9C4EC7;
$color-spaces:       #5EA73A;
$color-projects:     #00ADEB;
$color-opportunities:#003F7E;

// RedeMapas sobrescreve em theme-BaseV2.scss:
$color-primary:      #c46e14;  // dourado
$color-secondary:    #cc9933;  // laranja

// Responsividade
$desktop-breakpoint: 50rem;  // 800px
@mixin desktop { @media (min-width: $desktop-breakpoint) { @content; } }
@mixin mobile  { @media (max-width: $desktop-breakpoint) { @content; } }

Criar um novo componente CSS:

  1. Criar src/themes/BaseV2/assets-src/sass/2.components/_meu-componente.scss
  2. Importar em theme-BaseV2.scss
  3. Para override no RedeMapas: criar em src/themes/RedeMapas/assets-src/sass/ e importar lá

Workspace pnpm — como o build funciona

src/
├── package.json              # root — scripts: dev/build/watch --parallel --recursive
├── node_scripts/             # @mapas/scripts — esbuild-build.mjs, sass-compile-options.mjs
├── modules/Components/       # workspace package — compila os componentes Vue
├── themes/BaseV2/            # workspace package — compila o CSS base
└── themes/RedeMapas/         # workspace package (submodule) — home.js, sw.js, home.scss

Cada pacote tem seu próprio package.json com scripts build/dev/watch. O root orquestra com pnpm run --parallel --recursive build.

Build do RedeMapas (manual, dentro do submodule):

cd src/themes/RedeMapas
npm run build    # ou: pnpm run build (se não for submodule isolado)

Fontes em assets-src/ → output em assets/ (ignorado pelo git do submodule, mas versionado no repo pai via .gitignore customizado).


Grupos de scripts / estilos

Assets são enfileirados por grupo e impressos com printScripts('grupo') / printStyles('grupo'):

GrupoConteúdoImpresso por
vendor-v2Libs de terceirosLayout BaseV2
app-v2Vue init, componentes, temaLayout BaseV2
componentsTemplates Vue compiladosmapas.printJsObject:after
redemapas-homehome.js, push-notifications.js, home.csslayouts/home.php

Para enfileirar no tema:

// Em Theme.php — _init()
$this->enqueueScript('app-v2', 'meu-script', 'js/meu-script.js');
$this->enqueueStyle('app-v2', 'meu-style', 'css/meu-style.css');

// Com dependência
$this->enqueueScript('app-v2', 'meu-script', 'js/meu-script.js', ['outro-script']);

Hooks de template

// Injeta no <head>
$app->hook('template(<<*>>.head):end', function() { echo '<meta ...>'; });

// Injeta no início do <body>
$app->hook('template(<<*>>.body):begin', function() { echo '<div ...>'; });

// Injeta no final do <body> (antes de </body>)
$app->hook('template(<<*>>.body):end', function() { echo '<script>...</script>'; });

// Somente na home
$app->hook('template(site/index.body):end', function() { /* ... */ });

Stack de tecnologias

CamadaTecnologia
LinguagemPHP 8.3
Framework HTTPSlim 4
ORMDoctrine ORM + DBAL
BancoPostgreSQL 14 + PostGIS
CacheRedis
Build JS/CSSesbuild + dart-sass (pnpm workspaces)
ContainerDocker Compose
Reverse proxy localCaddy 2 (TLS interno)
Email (dev)Mailpit
AuthOpauth (LoginCidadao, Authentik, OpenID)
JobsJobType próprio do framework
TestesPHPUnit

Variáveis de ambiente relevantes

VariávelDescrição
DB_HOST/NAME/USER/PASSConexão PostgreSQL
REDIS_CACHEURL Redis
BASE_URLURL base da aplicação
APP_DEBUGLiga debug verboso
DISABLE_SUBSITESDesabilita subsites (true em dev)
REDEMAPAS_PUSH_ENABLEDLiga Web Push
REDEMAPAS_VAPID_PUBLIC_KEYChave pública VAPID
REDEMAPAS_VAPID_PRIVATE_KEYChave privada VAPID
REDEMAPAS_VAPID_SUBJECTE-mail/URL VAPID subject

Gerar chaves VAPID:

docker compose exec mapas php -r "
\$keys = \Minishlink\WebPush\VAPID::createVapidKeys();
echo 'Public:  ' . \$keys['publicKey'] . PHP_EOL;
echo 'Private: ' . \$keys['privateKey'] . PHP_EOL;
"

Contribuição

Branches

develop        → base para PRs
  └── feat/descricao-curta
  └── fix/descricao-curta

Commits

feat(scope): descrição    # nova funcionalidade
fix(scope): descrição     # correção de bug
chore: descrição          # manutenção

Submodule RedeMapas

Sempre commit em dois passos: primeiro dentro de src/themes/RedeMapas/, depois o ponteiro no repo pai.

Antes de abrir PR

  • Testes passando: docker compose exec mapas ./vendor/bin/phpunit tests/
  • Build de assets sem erros: cd src && pnpm run build
  • Sem erros no var/logs/error.log após smoke test manualmente

Esse material é fruto do Programa de Difusão Nacional - Funarte Redes das Artes, realizado pelo Laboratório do Futuro (entidade vinculada à Universidade Federal do Ceará) no ano de 2025.

Felicilab
Mutirão
Lab do Futuro UFC
UFC
Rede das Artes Funarte
Funarte
MinC Governo Federal

On this page