# Menus do Site — Plano de Migracao para Laravel Moderno (PHP 8.x / Laravel 11-12)

> Este documento descreve **equivalencia funcional**: o que **nao pode** mudar
> do ponto de vista de quem usa o portal e do operador do CMS.
>
> Todos os blocos de codigo deste arquivo sao **ilustrativos** (proposta para a
> v7); nao existe codigo equivalente no repositorio legado.

---

## 1. Schema de migracao equivalente

### 1.1 `menus`

```php
Schema::create('menus', function (Blueprint $table) {
    $table->id();
    $table->string('name');
    $table->string('slug')->unique();
    $table->boolean('status_create')->nullable();
    $table->timestamps();
    $table->softDeletes();
});
```

### 1.2 `menu_items`

```php
Schema::create('menu_items', function (Blueprint $table) {
    $table->id();
    $table->unsignedInteger('menus_id');
    $table->unsignedInteger('menus_items_id')->nullable();
    $table->string('name');
    $table->string('second_name')->nullable();
    $table->string('icon')->nullable();
    $table->unsignedInteger('order')->default(0);
    $table->unsignedTinyInteger('type')->nullable();
    $table->unsignedBigInteger('links_id')->nullable();
    $table->unsignedBigInteger('static_pages_id')->nullable();
    $table->unsignedBigInteger('category_contents_id')->nullable();
    $table->unsignedBigInteger('contents_id')->nullable();
    $table->boolean('isBlank')->default(false);
    $table->boolean('is_active')->default(true);
    $table->boolean('is_mega_menu')->default(false);
    $table->boolean('status_create')->nullable();
    $table->timestamps();
    $table->softDeletes();

    $table->foreign('menus_id')->references('id')->on('menus')->cascadeOnDelete();
    $table->foreign('menus_items_id')->references('id')->on('menu_items')->nullOnDelete();
    $table->foreign('links_id')->references('id')->on('links')->nullOnDelete();
    $table->foreign('static_pages_id')->references('id')->on('static_pages')->nullOnDelete();
    $table->foreign('category_contents_id')->references('id')->on('category_contents')->nullOnDelete();
    $table->foreign('contents_id')->references('id')->on('contents')->nullOnDelete();

    $table->index(['menus_id', 'menus_items_id', 'order']);
});
```

Mudancas em relacao ao legado (todas **justificadas**):

| Mudanca | Motivo |
|---|---|
| `order` com `default(0)` | No legado a coluna e `NOT NULL` sem default, mas o `FormRequest` remove o campo do request — a v7 garante o default |
| Indice `(menus_id, menus_items_id, order)` | Elimina o N+1 da arvore e das consultas do portal |
| `menus_items_id` com `nullOnDelete` | Evita erro de FK quando um pai e removido (o legado bloqueia a exclusao) |
| `slug` unico em `menus` | O legado gera `str_slug(name)` sem garantir unicidade |
| `softDeletes` mantido | Coerencia com `Menu`; `MenuItem` passa a usar `SoftDeletes` de fato |
| Coluna `link` **nao** recriada | Nao existe no banco da v6; o destino de um item tipo 0 vive em `links` |
| `status_create` mantido | Ainda ordena a listagem (view `menus_site`) |

### 1.3 `menu_orders`

```php
Schema::create('menu_orders', function (Blueprint $table) {
    $table->id();
    $table->string('slug')->unique();
    $table->unsignedInteger('clicks')->default(0);
    $table->unsignedBigInteger('modulo_id')->nullable();
    $table->timestamps();
    $table->softDeletes();
    $table->foreign('modulo_id')->references('id')->on('modulos')->nullOnDelete();
});
```

### 1.4 View `menus_site`

Manter a view (o `DatatableController` legado depende dela) ou substitui-la por
uma query Eloquent. Se mantida:

```sql
CREATE OR REPLACE VIEW menus_site AS
SELECT row_number() OVER (ORDER BY (m.status_create is true) DESC) AS value,
       m.id, m.name, m.slug
FROM menus m
WHERE m.deleted_at IS NULL
ORDER BY (m.status_create is true) DESC;
```

Recomendacao v7: **eliminar a view** e expor um `MenuResource` paginado — a
view nao agrega valor alem do `row_number()`.

---

## 2. Modelos

```php
enum MenuItemType: int
{
    case Link = 0;
    case StaticPage = 1;
    case Content = 2;
}

class Menu extends Model
{
    use SoftDeletes;

    protected $fillable = ['name', 'slug', 'status_create'];

    protected function casts(): array
    {
        return ['status_create' => 'boolean'];
    }

    public function items(): HasMany
    {
        return $this->hasMany(MenuItem::class, 'menus_id')->orderBy('order');
    }

    public function rootItems(): HasMany
    {
        return $this->hasMany(MenuItem::class, 'menus_id')
            ->whereNull('menus_items_id')
            ->orderBy('order');
    }
}

class MenuItem extends Model
{
    use SoftDeletes;

    protected $fillable = [
        'menus_id', 'menus_items_id', 'name', 'second_name', 'icon',
        'order', 'type', 'links_id', 'static_pages_id',
        'category_contents_id', 'contents_id', 'isBlank',
        'is_active', 'is_mega_menu', 'status_create',
    ];

    protected function casts(): array
    {
        return [
            'type' => MenuItemType::class,
            'isBlank' => 'boolean',
            'is_active' => 'boolean',
            'is_mega_menu' => 'boolean',
        ];
    }

    public function menu(): BelongsTo { return $this->belongsTo(Menu::class, 'menus_id'); }
    public function parent(): BelongsTo { return $this->belongsTo(self::class, 'menus_items_id'); }
    public function children(): HasMany { return $this->hasMany(self::class, 'menus_items_id')->orderBy('order'); }
    public function link(): BelongsTo { return $this->belongsTo(Link::class, 'links_id'); }
    public function staticPage(): BelongsTo { return $this->belongsTo(StaticPage::class, 'static_pages_id'); }
    public function category(): BelongsTo { return $this->belongsTo(CategoryContent::class, 'category_contents_id'); }
    public function content(): BelongsTo { return $this->belongsTo(Content::class, 'contents_id'); }

    public function isRoot(): bool { return $this->menus_items_id === null; }
}

class MenuOrder extends Model
{
    use SoftDeletes;

    protected $fillable = ['slug', 'clicks', 'modulo_id'];

    protected function casts(): array { return ['clicks' => 'integer']; }

    public function module(): BelongsTo { return $this->belongsTo(Modulo::class, 'modulo_id'); }
}
```

### Mudancas funcionais a decidir (e documentar)

| Legado | v7 | Risco |
|---|---|---|
| `Menu::menuItems()` = `belongsTo` | `Menu::items()` = `hasMany` | Correcao; nenhum consumidor legado depende da relacao errada |
| `MenuItem` sem `SoftDeletes` | com `SoftDeletes` | **Exige backfill**: `UPDATE menu_items SET deleted_at = now() WHERE deleted_at IS NULL` — nao ha historico de exclusao no legado, entao a migracao e neutra. Mas o CMS passa a **nao** excluir fisicamente: e preciso decidir se a UI passa a usar `forceDelete` ou *soft delete* |
| `type` inteiro solto | enum `MenuItemType` | Serializacao do enum no `API Resource` deve ser explicita (`->value`) |
| `is_father` (accessor) | metodo `isRoot()` | O accessor na v7 pode ser mantido como alias para nao quebrar templates |
| `isBlank` (camelCase) | `is_blank` (snake) | Exige migracao de dados + renomeacao em `links`, `links` tem a mesma coluna |

> A coluna `isBlank` existe **tambem** em `links` e em `contents`; renomear
> apenas em `menu_items` quebraria o `MenuItemPresenter`, que le
> `$this->links->isBlank`. Decisao: manter `isBlank` como esta, ou renomear em
> todo o sistema.

---

## 3. Resolucao de URL (substituir o presenter)

O `MenuItemPresenter` vira um servico puro e testavel:

```php
final class MenuLinkResolver
{
    public function __construct(private CategoryContentRepository $categories) {}

    public function resolve(MenuItem $item): string
    {
        return match ($item->type) {
            MenuItemType::Link => $item->link?->link ?? '#',
            MenuItemType::StaticPage => $item->static_pages_id === 4
                ? url($item->staticPage->slug)
                : url('acesso-rapido/' . $item->staticPage->slug),
            MenuItemType::Content => $this->resolveContent($item),
        };
    }
}
```

**Equivalencia obrigatoria**: os ids fixos de categoria (2 = legislacao,
3 = publicacoes, 7 e 32 = a-instituicao, 12 = acesso-rapido) e o slug
`conselhos`. A forma correta e substituir esses ids por **configuracao**
(slug da categoria) em `configurations`, com fallback para os ids legados
durante a transicao:

```php
// config/menu.php
'category_routes' => [
    3  => 'publicacoes',
    2  => 'legislacao',
    7  => 'a-instituicao',
    32 => 'a-instituicao',
    12 => 'acesso-rapido',
],
```

---

## 4. Form Requests

```php
class StoreMenuItemRequest extends FormRequest
{
    public function authorize(): bool
    {
        return $this->user()->can('menu.create');
    }

    public function rules(): array
    {
        $maxRoots = config('menu.max_root_items', 6);

        return [
            'menus_id' => ['required', 'integer', 'exists:menus,id'],
            'menus_items_id' => [
                'nullable', 'integer',
                Rule::exists('menu_items', 'id')->where('menus_id', $this->input('menus_id')),
            ],
            'name' => ['required', 'string', 'max:191'],
            'second_name' => ['nullable', 'string', 'max:191'],
            'icon' => ['nullable', 'string', 'max:191'],
            'type' => ['required', Rule::enum(MenuItemType::class)],
            'link' => ['required_if:type,0', 'nullable', 'url_or_path'],
            'static_pages_id' => ['required_if:type,1', 'exists:static_pages,id'],
            'category_contents_id' => ['required_if:type,2', 'exists:category_contents,id'],
            'contents_id' => ['nullable', 'exists:contents,id'],
            'isBlank' => ['boolean'],
            'is_mega_menu' => ['boolean'],
        ];
    }

    public function withValidator(Validator $validator): void
    {
        $validator->after(function (Validator $validator) {
            if (! $this->isCreating() || ! $this->wantsParent()) {
                return;
            }

            $roots = MenuItem::where('menus_id', $this->input('menus_id'))
                ->whereNull('menus_items_id')
                ->count();

            if ($roots >= config('menu.max_root_items', 6) && ! $this->input('menus_items_id')) {
                $validator->errors()->add('menus_items_id', 'Limite máximo de itens atingido, escolha um item pai para sub-itens.');
            }
        });
    }
}
```

Pontos a preservar exatamente:

1. A mensagem de erro do limite de 6 ("Limite máximo de itens atingido, escolha um
   item pai para sub-itens.") — ja aparece no legado.
2. A regra do limite vale **so na criacao** (o legado so aplica na rota
   `menu-item.store`).
3. `order` **nao** vem do request.

---

