betocampoy / champs-onboarding
Onboarding guiado (tours de interface) para projetos Symfony, integrado ao champs-frontend.
Package info
github.com/betocampoy/champs-onboarding
Type:symfony-bundle
pkg:composer/betocampoy/champs-onboarding
Requires
- php: >=8.2
- betocampoy/champs-frontend: ^1.10
- doctrine/doctrine-bundle: ^2.13
- doctrine/orm: ^3.0
- symfony/console: ^7.4
- symfony/finder: ^7.4
- symfony/form: ^7.4
- symfony/framework-bundle: ^7.4
- symfony/messenger: ^7.4
- symfony/security-bundle: ^7.4
- symfony/translation: ^7.4
- symfony/twig-bundle: ^7.4
- symfony/validator: ^7.4
Requires (Dev)
None
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
Onboarding guiado (tours de interface) para projetos Symfony 7.4, integrado ao
champs-frontend.
O pacote traz as próprias entidades e tabelas (champs_onboarding_*).
O projeto consumidor não precisa adaptar nenhuma entidade: o progresso é
gravado pelo userIdentifier do usuário logado (UserInterface::getUserIdentifier()).
Estrutura
src/
├── ChampsOnboardingBundle.php # registra config e mapeamento Doctrine
├── Entity/
│ ├── Tour.php # champs_onboarding_tour
│ ├── TourStep.php # champs_onboarding_step
│ └── TourProgress.php # champs_onboarding_progress
├── Command/SyncMonitoredToursCommand.php # champs:onboarding:sync
├── Controller/OnboardingController.php # endpoints JSON
├── EventListener/
│ ├── TourMonitoringListener.php # tour virou monitorado → agenda sync
│ └── UserLifecycleListener.php # User criado/alterado/excluído
├── EventSubscriber/MandatoryOnboardingSubscriber.php
├── Exception/OnboardingException.php
├── Manager/
│ ├── OnboardingManager.php # regras do tour
│ ├── MonitoringManager.php # linhas "não iniciado"
│ └── OnboardingStats.php # números do dashboard
├── Message/SyncMonitoredTour.php
├── MessageHandler/SyncMonitoredTourHandler.php
├── Monitoring/MonitoredUserProvider.php
├── Enum/
│ ├── TourTrigger.php # first_access | new_feature | manual
│ ├── ProgressStatus.php # pending | in_progress | completed | skipped
│ └── StepPosition.php # top | bottom | left | right | auto
└── Repository/
├── TourRepository.php
├── TourStepRepository.php
└── TourProgressRepository.php
config/
├── routes.php
└── services.php
Instalação durante o desenvolvimento (repositório path)
No composer.json do projeto (ex.: MEMOD):
"repositories": [ { "type": "path", "url": "../champs-onboarding", "options": { "symlink": true } } ]
composer require betocampoy/champs-onboarding:@dev
Quando estabilizar, troque o path por vcs apontando para o GitHub e use tags de versão.
Registro do bundle
config/bundles.php:
BetoCampoy\Champs\Onboarding\ChampsOnboardingBundle::class => ['all' => true],
config/packages/champs_onboarding.yaml (opcional):
champs_onboarding: admin_role: ROLE_ADMIN
Exige betocampoy/champs-frontend ^1.7.
Banco de dados
O mapeamento Doctrine é registrado pelo próprio bundle. Basta gerar e rodar a migration:
php bin/console doctrine:migrations:diff php bin/console doctrine:migrations:migrate
Leia a migration gerada. Ela deve criar só champs_onboarding_tour, champs_onboarding_step
e champs_onboarding_progress. Se o banco do projeto já tiver drift em relação ao mapeamento,
o diff arrasta junto ALTER/DROP de tabelas do projeto: nesse caso, limpe à mão.
Rotas
config/routes/champs_onboarding.yaml:
champs_onboarding: resource: '@ChampsOnboardingBundle/config/routes.php' prefix: /onboarding
| Método | Rota | Uso |
|---|---|---|
| GET | /onboarding/tour?route=app_x |
Tour a exibir nesta rota (retoma ou inicia) |
| GET | /onboarding/tour/{slug} |
Abre/reabre manualmente (botão "?") |
| POST | /onboarding/progress |
{tour, step, action} com action next, complete ou skip |
| GET | /onboarding/available?route=app_x |
Tours da página para o menu de ajuda |
No next, step é o passo em que o usuário estava ao clicar. No último passo, next conclui o tour.
Erros de regra voltam como JSON {error} em português (401 sem login, 404 tour inexistente,
403 sem acesso, 422 ação/passo/corpo inválido, 409 tour não iniciado).
- Sem login os endpoints respondem 401 em JSON (não redirecionam). Se o
access_controldo projeto exigir login em/onboarding, o firewall redireciona antes. - CSRF: o
POST /progressexige o cabeçalhoX-Champs-Ajax(o mesmo do AjaxForm do champs-frontend) e recusaSec-Fetch-Site: cross-site(403). Outro site não consegue enviar cabeçalho customizado sem preflight CORS.
As rotas só existem pelo import acima. O config/routes.yaml padrão do Symfony 7.4
(resource: routing.controllers) importaria todo controller com #[Route], inclusive os do
bundle e sem prefixo; o ExcludeFromRoutingControllersPass tira os controllers do bundle dessa descoberta.
Quem vê cada tour (elegibilidade)
Tour::requiredAttribute é um atributo de segurança (null = qualquer usuário logado). A regra
fica num único serviço, TourEligibilityCheckerInterface, usado tanto nos endpoints quanto
no monitoramento (worker/comando, sem sessão).
O padrão (AuthorizationEligibilityChecker) chama isGrantedForUser($user, $atributo, $tour):
ROLE_Xfunciona sem configuração e respeita arole_hierarchy- qualquer outro atributo é respondido pelos voters do projeto (o
Tourvai como subject). Ex.:perm:fatura.listar,modulo:financeiro. Os voters não podem depender da sessão.
Para outra regra (ex.: excluir usuários desativados), implemente a interface no projeto
(pode receber o AuthorizationEligibilityChecker e complementar) e aponte o alias:
#[AsAlias(TourEligibilityCheckerInterface::class)] final class MinhaRegra implements TourEligibilityCheckerInterface { /* ... */ }
Segmento (estatísticas por tenant, unidade, plano...)
Os tours são globais. Para filtrar e agrupar as estatísticas, implemente
UserSegmentResolverInterface (resolve($user): ?string e label($segment): string) e aponte
o alias do mesmo jeito. O segmento é gravado em champs_onboarding_progress.segment:
quando o usuário usa o tour, quando muda um dos watch_fields e em cada champs:onboarding:sync
(que também corrige segmentos desatualizados). Sem implementação, fica tudo sem segmento.
Tour obrigatório
Marque mandatory = true no tour. A partir daí:
skipé recusado no backend (HTTP 422) e o front esconde o botão "Pular"- o
MandatoryOnboardingSubscriberredireciona qualquer navegação GET para a página do tour até ele ser concluído (AJAX, JSON, rotas do onboarding e rotas isentas passam livres) - a rota inicial do tour obrigatório não pode ter parâmetros obrigatórios
champs_onboarding: mandatory: enabled: true exempt_routes: [app_logout, app_termos] exempt_route_prefixes: ['_'] cache_seconds: 300
Quando o usuário não tem pendência, isso fica guardado na sessão por cache_seconds.
Um tour obrigatório criado depois leva até esse tempo para começar a ser cobrado de quem já está logado.
Tours monitorados
Marque monitored = true no tour. O bundle cria uma linha não iniciado (pending)
para cada usuário elegível e vai atualizando conforme o usuário abre, conclui ou pula.
Assim o dashboard mostra também quem nunca abriu o tour.
champs_onboarding: monitoring: user_class: App\Entity\User identifier_property: email # propriedade por trás do getUserIdentifier() watch_fields: [roles] # o que muda a elegibilidade ou o segmento batch_size: 500
Sem user_class, o monitoramento fica desligado e o resto do bundle funciona normalmente.
Elegível = o TourEligibilityCheckerInterface diz que sim (ver acima).
watch_fields: campos, associações to-one ou coleções (ManyToMany/OneToMany) do User.
Mudança em qualquer um ressincroniza o usuário. Inclua tudo de que a elegibilidade e o segmento
dependem (ex.: [tenant, authRoles, status]).
Carga inicial: ao salvar um tour monitorado (ou mudar monitored, active ou
requiredAttribute), o TourMonitoringListener despacha SyncMonitoredTour no Messenger.
Mudanças fora do User (ex.: o tenant contratou um módulo): despache
SyncMonitoredUsers($segmento) para ressincronizar os usuários daquele segmento
(ou null para todos).
Roteie as duas mensagens para um transport assíncrono:
# config/packages/messenger.yaml framework: messenger: routing: BetoCampoy\Champs\Onboarding\Message\SyncMonitoredTour: async BetoCampoy\Champs\Onboarding\Message\SyncMonitoredUsers: async
Sem roteamento, a mensagem é processada na hora (síncrona).
Ciclo de vida do usuário (listener do Doctrine na user_class, sem código no projeto):
| Evento | O que acontece |
|---|---|
| Usuário criado | Cria pending nos tours monitorados que ele pode ver |
Algum watch_fields alterado |
Cria pending nos que passou a ver, remove pending dos que deixou de ver e atualiza o segmento |
| Identifier alterado (ex.: e-mail) | Renomeia as linhas: o progresso acompanha o usuário |
| Usuário excluído | Remove todas as linhas dele |
Histórico de quem já abriu o tour nunca é apagado por mudança de acesso. Uma falha no monitoramento só é registrada no log: nunca impede salvar o usuário.
Comando (para ressincronizar ou depois de importar usuários direto no banco):
php bin/console champs:onboarding:sync # todos os monitorados php bin/console champs:onboarding:sync boas-vindas # um tour php bin/console champs:onboarding:sync --async # só enfileira
Estatísticas
OnboardingStats entrega os dados do dashboard:
overview(?segment): por tour — não iniciados, iniciados, em andamento, concluídos, pulados, taxa de conclusão (sobre quem abriu), cobertura (sobre todos os elegíveis, só em monitorados), tempo médio e reaberturasforTour($tour, ?segment): o resumo acima + funil por passo + atividade recente + lista de quem não abriubySegment($tour): o resumo do tour por segmento (com olabel), do maior para o menor
segment = null nos dois primeiros = todos os segmentos.
Front (champs-frontend ≥ 1.8)
O módulo Onboarding.js do champs-core-js já vem no initCore(). No layout das páginas logadas:
{# raiz: uma por página #} <div class="d-none" data-champs-onboarding data-champs-onboarding-route="{{ app.request.attributes.get('_route') }}" data-champs-onboarding-url="{{ path('champs_onboarding_tour')|slice(0, -5) }}"></div> {# botão de ajuda: reabre / lista os tours da página #} <button type="button" class="btn btn-link" data-champs-onboarding-help><i class="bi bi-question-circle"></i></button>
Âncoras dos passos (o valor é o campo anchor do TourStep):
<button data-champs-tour="btn-importar">Importar</button>
Textos traduzidos, atributos e eventos: ver o README do champs-core-js (seção Onboarding).
Cadastro de tours (admin)
Telas em /onboarding/admin/tours (prefixo do import de rotas), só para admin_role:
lista com busca e números de uso, cadastro do tour, passos (criar, editar, subir/descer, excluir)
e Testar tour (modo teste do Onboarding.js: nada é gravado, funciona com tour inativo).
champs_onboarding: admin_role: ROLE_ADMIN admin: layout: admin/layout.html.twig # precisa ter o bloco "content" form_theme: '@ChampsFrontend/form/champs_theme.html.twig' route_path_prefixes: [/app] # telas oferecidas no select (vazio = todas) anchor_paths: ['%kernel.project_dir%/templates'] # onde procurar data-champs-tour switch_user_parameter: _switch_user # liga "Testar como…"/"Apontar como…" (null = desligado)
- Toda rota GET aparece como tela de tour, inclusive as de um registro (
/remessas/{id}): o tour dispara pelo nome da rota, então aparece quando o usuário abre qualquer registro daquela tela e ensina em cima do dado real. O front não consegue levar o usuário até uma tela de registro (não sabe qual), então o passo seguinte numa tela dessas avisa "continua em outra tela". - URL de exemplo (
Tour::sampleUrl): URL de um registro real da tela do 1º passo, usada só pelo admin no Testar/Testar como/Apontar dessas telas. O cadastro confere se ela abre a tela certa (RouteCatalog::routeOfUrl()). Tour obrigatório não pode começar em tela de registro. - As âncoras
data-champs-tourjá usadas nos templates são sugeridas no cadastro do passo; âncora digitada que não existe em nenhum template aparece com aviso. - Telas e textos (
champs_onboarding.*.yaml, pt_BR/en/es) podem ser sobrescritos pelo mecanismo padrão do Symfony (templates/bundles/ChampsOnboardingBundle/,translations/). - Âncora do passo: nome do
data-champs-tourou seletor CSS (até 255 caracteres; regra emTourStep::isSelectorAnchor()). Os componentes do champs-frontend ≥ 1.9 já geram âncoras (field-<id>,page-title,list-content…), então a maioria dos tours não exige mexer em template. - Apontar na tela (cadastro do passo): abre a tela do passo numa aba nova em modo apontar (clique normal usa a tela, Ctrl+clique escolhe; avisa se o elemento foi escolhido em outra tela); o clique no elemento devolve a âncora para o campo (a mais estável disponível; caminho no DOM aparece como frágil).
- Opções do passo:
- Avança ao clicar no elemento — o próprio elemento destacado avança (ex.: botão que abre um modal).
- Exige digitar no campo (+ texto esperado, opcional) — o "Próximo" só libera com o campo preenchido; Enter avança e deixa o formulário seguir (ex.: tour de pesquisa). Precisa de âncora; não combina com "avança ao clicar".
- Fechar modal aberto ao chegar neste passo — para seguir com um elemento fora do modal.
- Testar como… / Apontar como…: com
admin.switch_user_parameter(ex.:_switch_user) emonitoring.user_class, a tela abre personificando um usuário escolhido entre os que podem ver o tour (/onboarding/admin/tours/{id}/usuarios?q=). É o caminho para telas que o admin não acessa. O "Apontar como…" sai da personificação sozinho ao terminar; o "Testar como…" volta pelo "sair da personificação" do projeto. - Testar tour abre
?champs_onboarding_preview=<slug>na tela do 1º passo. O tour é lido por/onboarding/admin/tours/preview/{slug}, liberado para o admin e para um admin que esteja personificando um usuário (switch_user): é o caminho para testar tours de telas que o admin não acessa. A tela do tour mostra o link de teste para copiar.
Próximas etapas
- Dashboard de estatísticas (
OnboardingStats) nas telas do admin