# Glossário — Migração para Laravel Moderno (PHP 8.x + Laravel 11/12)

> Plano de reconstrução do módulo. Decisões de equivalência funcional estão marcadas
> como **DECISÃO** e são o que não pode mudar.

---

## 1. Schema de migração Laravel

A v6 não tem tabela de verbetes. A v7 **deve** criar uma tabela própria, isolando o
glossário do conteúdo genérico:

```php
// exemplo ilustrativo — migration nova na v7
Schema::create('glossary_terms', function (Blueprint $table) {
    $table->id();
    $table->string('term', 191);
    $table->string('normalized_term', 191)->index();
    $table->text('definition');
    $table->boolean('status')->default(true)->index();
    $table->timestamps();
    $table->softDeletes();

    $table->unique(['term', 'deleted_at']);
});
```

**DECISÃO 1 — o verbete continua sendo "termo + definição".** A migração de dados deve
mapear `contents.title` -> `term` e `contents.text` -> `definition` para os registros com
`category_contents_id = 11`.

Alternativa de menor risco: manter a tabela `contents` e apenas acrescentar uma view
dedicada. É o que a v6 faz e evita reescrever a busca global. O trade-off é manter o
acoplamento ao CRUD genérico.

Script de migração de dados (exemplo ilustrativo):

```php
DB::table('contents')
    ->where('category_contents_id', $glossarioId)
    ->whereNull('deleted_at')
    ->orderBy('id')
    ->chunk(500, function ($contents) use (&$glossaryId) {
        foreach ($contents as $c) {
            DB::table('glossary_terms')->insert([
                'term' => $c->title,
                'normalized_term' => Str::ascii(Str::lower($c->title)),
                'definition' => $c->text,
                'status' => (bool) $c->status,
                'created_at' => $c->created_at,
                'updated_at' => $c->updated_at,
                'deleted_at' => $c->deleted_at,
            ]);
        }
    });
```

## 2. Models

| Model | Tabela | Atributos / casts |
|---|---|---|
| `GlossaryTerm` | `glossary_terms` | `$fillable = ['term','definition','status']`; cast `status` => `bool`; `SoftDeletes` |
| `GlossaryTermPresenter` (se mantido o padrão da plataforma) | — | Formata `term`/`definition` |

```php
// exemplo ilustrativo
class GlossaryTerm extends Model
{
    use SoftDeletes;

    protected $fillable = ['term', 'definition', 'status'];

    protected $casts = ['status' => 'boolean'];

    public function scopeStartingWith(Builder $query, string $letter): Builder
    {
        return $query->where('normalized_term', 'like', mb_strtolower($letter) . '%');
    }

    public function scopePublished(Builder $query): Builder
    {
        return $query->where('status', true);
    }
}
```

**DECISÃO 2 — o índice alfabético deve continuar filtrando por inicial do termo.**
A normalização por `Str::ascii` + `lower` corrige o bug de maiúsculas/acento do legado
sem mudar o comportamento percebido (a letra clicada continua significando "início do termo").

## 3. Form Requests

```php
// exemplo ilustrativo
class StoreGlossaryTermRequest extends FormRequest
{
    public function authorize(): bool
    {
        return $this->user()->can('create', GlossaryTerm::class);
    }

    public function rules(): array
    {
        return [
            'term' => ['required', 'string', 'max:120'],
            'definition' => ['required', 'string'],
            'status' => ['boolean'],
        ];
    }
}
```

**DECISÃO 3 — `definition` passa a ser obrigatória no servidor.** No legado o campo é
obrigatório apenas no markup (`form.blade.php`); a v7 deve manter a exigência visível
e alinhá-la com a validação.

## 4. Policies / autorização

| Ação | Permissão legada | Policy v7 |
|---|---|---|
| Listar (portal) | — | pública |
| Criar/editar/excluir (CMS) | `glossario.create`, `glossario.edit`, `glossario.delete` | `GlossaryTermPolicy` (`create`, `update`, `delete`) |

As flags `edit`/`delete`/`active`/`inactivate` injetadas por linha em
`DatatableController@struct_views` são substituídas por `@can` na Blade.

## 5. Endpoints REST/JSON sugeridos

| Método | URI | Descrição |
|---|---|---|
| GET | `/api/v1/glossario` | Lista paginada (`?letra=A`, `?q=termo`, `?per_page=50`) |
| GET | `/api/v1/glossario/{id}` | Verbete único |
| POST | `/api/v1/glossario` | Cria (CMS/integração) |
| PUT | `/api/v1/glossario/{id}` | Atualiza |
| DELETE | `/api/v1/glossario/{id}` | Exclui (soft delete) |
| GET | `/glossario` | Página pública (HTML) |
| GET | `/glossario/letra/{letra}` | Alternativa server-side ao AJAX legado |

**DECISÃO 4 — a rota pública `/glossario` e o parâmetro de letra permanecem.**
Apps e links externos do município apontam para `/glossario`.

## 6. Recursos de API

```php
// exemplo ilustrativo
class GlossaryTermResource extends JsonResource
{
    public function toArray($request): array
    {
        return [
            'id' => $this->id,
            'term' => $this->term,
            'definition' => $this->definition,
        ];
    }
}
```

O recurso substitui o JSON cru do legado, que expunha o model `Content` inteiro.

## 7. Views Blade modernas

| Antes (v6) | Depois (v7) |
|---|---|
| 8 arquivos de portal com o mesmo HTML/JS | 1 componente `<x-glossario.index :letters="$letters" />` + 1 `<x-glossario.term>` |
| JS inline em cada layout | `resources/js/glossario.js` (Alpine ou Stimulus) |
| `$contents` server-side apenas no layout `default` | Controller entrega os termos do lado do servidor (SSR) ou via API + cache |

Exemplo de componente (exemplo ilustrativo):

```blade
{{-- resources/views/components/glossario/index.blade.php --}}
<div class="btn-group" role="group" aria-label="Letras">
    @foreach ($letters as $letter)
        <button type="button" class="btn-letter @if($activeLetter === $letter) active @endif"
                data-letter="{{ $letter }}">{{ $letter }}</button>
    @endforeach
</div>
<ul class="list-unstyled content-glossary">
    @foreach ($terms as $term)
        <li><h5>{{ $term->term }}</h5><p>{!! $term->definition !!}</p></li>
    @endforeach
</ul>
```

**DECISÃO 5 — o `text`/definição continua sendo HTML renderizado sem escape**
(`{!! !!}`), porque o CMS insere formatação (negrito, links, listas). Na v7, sanitizar
o HTML na gravação (HTMLPurifier) e manter a renderização do conteúdo sanitizado.

## 8. Fila, cache e storage

| Aspecto | Recomendação |
|---|---|
| Cache | Cachear a lista de termos (`Cache::remember('glossario:all', 300, fn () => GlossaryTerm::published()->get())`) e invalidar no save/delete do model |
| Fila | Nenhuma tarefa assíncrona é necessária (não há e-mail, PDF ou thumbnail) |
| Storage | Se o glossário aceitar anexos no futuro, usar `storage/app/public/glossario` com `Storage::disk('public')` |
| Busca | Integrar o glossário ao índice de busca (Scout/Meilisearch/PG full-text) mantendo o comportamento de normalização |

## 9. Riscos e decisões de equivalência funcional

| # | Risco | Mitigação |
|---|---|---|
| R1 | Perder a nomenclatura `glossario` (slug de módulo, categoria e permissão) | Manter o slug e criar aliases para os ids antigos |
| R2 | Mudar o mapeamento termo/definição quebra os verbetes já publicados | Teste de migração com contagem de linhas idêntica |
| R3 | Busca global depende do id 11 em `getViewToBeReturned` | Substituir ids por slugs na v7 |
| R4 | Mudar o comportamento do índice (ex.: busca por ocorrência) altera o que o cidadão vê | Manter filtro por inicial (DECISÃO 2) |
| R5 | HTML sem escape é um risco de XSS | Sanitizar na entrada (DECISÃO 5) |
| R6 | Layout `default` nunca funcionou para `/glossario` | Na v7 há um único template: o bug desaparece |
| R7 | Duplicação de view por layout (6 modelos + newsbit) | Componente único |
