# Bongo Menu Package - Cursor AI Rules

## Project Overview

Laravel package for creating and managing hierarchical frontend menus. Menu items can link to internal entities (pages, posts, projects, categories) or external URLs. Built on bongo/framework with full CRUD via backend UI and REST API.

## Project Structure

```
menu/
├── src/
│   ├── Config/
│   │   └── menu.php                    # Entity/route configuration
│   ├── Events/
│   │   └── MenuEventHandler.php        # Auto-generates keys, clears cache
│   ├── Http/
│   │   ├── Controllers/
│   │   │   ├── Api/                    # MenuController, MenuItemController
│   │   │   └── Backend/                # MenuController, MenuDatatableController
│   │   ├── Requests/
│   │   │   ├── Api/                    # Store/Update validation
│   │   │   └── Backend/                # Store/Update validation
│   │   ├── Resources/                  # JSON API resources
│   │   └── ViewComposers/
│   │       └── MenuComposer.php        # Caches menu data for views
│   ├── Listeners/
│   │   └── ClearMenuCache.php          # Cache management
│   ├── Migrations/
│   │   ├── 2019_01_01_000001_create_menus_table.php
│   │   └── 2019_01_01_000002_create_menu_items_table.php
│   ├── Models/
│   │   ├── Menu.php                    # HasStatus, HasUUID, SoftDeletes
│   │   └── MenuItem.php                # HasRecursive, HasUUID
│   ├── Routes/
│   │   ├── api.php                     # /api/menus, /api/menu-items
│   │   └── backend.php                 # /admin/menu
│   ├── Seeders/
│   │   ├── DataSeeder.php
│   │   └── PackageSeeder.php
│   ├── Services/
│   │   ├── MenuEntityType.php          # Dynamic entity type discovery
│   │   └── MenuItemService.php         # URL generation, grouping
│   ├── Translations/
│   │   └── en/backend.php
│   ├── Views/
│   │   ├── backend/                    # Admin CRUD views
│   │   └── frontend/partials/          # Menu templates
│   └── MenuServiceProvider.php
├── database/factories/
│   ├── MenuFactory.php
│   └── MenuItemFactory.php
├── tests/
│   ├── Feature/
│   ├── Unit/
│   └── TestCase.php                    # Orchestra Testbench setup
├── composer.json
├── pint.json                           # Laravel Pint config
└── phpstan.neon.dist                   # PHPStan config (level 1)
```

## Architecture Patterns

### Service Provider Pattern

**MenuServiceProvider** extends `Bongo\Framework\Providers\AbstractServiceProvider` which auto-registers:

- **Config**: From `src/Config/menu.php`
- **Routes**:
  - `api.php` → `/api/*` with `auth:sanctum` middleware
  - `backend.php` → `/admin/menu/*` with `auth` + `employee` middleware
- **Views**: From `src/Views/menu/` (namespace: `menu::`)
- **Migrations**: From `src/Migrations/`
- **Translations**: From `src/Translations/`

**Key configuration**:
```php
protected string $module = 'menu';  // Defines view namespace

protected array $composers = [
    MenuComposer::class => [
        'menu::frontend.partials.menu',
        'menu::frontend.partials.top_menu',
        'menu::frontend.partials.main_menu',
        'menu::frontend.partials.footer_menu',
        'menu::frontend.partials.footer_menu_2',
        'menu::frontend.partials.bottom_menu',
    ],
];

protected array $subscribers = [
    MenuEventHandler::class,
];
```

**Facade**: MenuEntityType is aliased in `boot()` method.

### Database Architecture

**menus table**:
- `id`, `uuid`, `name`, `key` (slug), `status` (enum: pending/active/inactive)
- Soft deletes, audit fields (created_by, updated_by, deleted_by)

**menu_items table**:
- `id`, `uuid`, `menu_id` (FK to menus, cascade delete), `parent_id` (nullable, self-referencing)
- `name`, `url` (for external links), `class` (CSS class)
- `entity_type`, `entity_id` (polymorphic linking)
- `type` (enum: internal/external), `target` (enum: _self/_blank)
- `sort_order` (indexed)

### Model Relationships & Traits

**Menu Model**:
- Traits: `HasFactory`, `HasStatus`, `HasUUID`, `SoftDeletes`
- `items()` - HasMany MenuItem (only `parent_id` IS NULL, ordered by `sort_order`)
- `allItems()` - HasMany MenuItem (includes nested, ordered by `sort_order`)
- `hasItems()` - Boolean check
- `clearCache()` - Clears menu cache + all nested item entity caches

**MenuItem Model**:
- Traits: `HasFactory`, `HasRecursive`, `HasUUID`
- `HasRecursive` provides: `parent()`, `children()`, `ancestors()`, `descendants()`
- `menu()` - BelongsTo Menu
- Type constants: `EXTERNAL`, `INTERNAL`, `SELF`, `BLANK`

### Entity Configuration System

**config/menu.php** maps entity types to models and routes:

```php
'entities' => [
    'page' => Page::class,
    'post' => Post::class,
    'document' => Document::class,  // Special: treated as asset
    'post_category' => PostCategory::class,
    'project' => Project::class,
    'project_category' => ProjectCategory::class,
],
'routes' => [
    'page' => 'frontend.page.show',
    'post' => 'frontend.post.show',
    // Note: 'document' has NO route (uses asset() instead)
    'post_category' => 'frontend.post_category.show',
    'project' => 'frontend.project.show',
    'project_category' => 'frontend.project_category.show',
]
```

**Link resolution flow**:
1. `entity_type` (e.g., "Post") → `getEntityType()` → "post"
2. `getEntityInstance()` → `config('menu.entities.post')` → `Post::class`
3. `getEntity()` → `Post::find($entity_id)` (cached)
4. `getEntityRoute()` → `config('menu.routes.post')` → `'frontend.post.show'`
5. `MenuItemService::routeLink()` → `route('frontend.post.show', $entity->slug)`

**Special case**: Documents use `isAsset()` → returns `asset($entity->src)` instead of routing.

**To add new entity type**: Add to both arrays, ensure entity has `slug` attribute.

### MenuItem URL Resolution

**MenuItem::getLink()** chain:
1. External link → returns `url` field
2. Document asset → returns `asset($entity->src)`
3. Internal entity → delegates to `MenuItemService::routeLink()` → generates `route($routeName, $entity->slug)`
4. Fallback → returns `'#'`

**Key methods**:
- `isInternal()`, `isExternal()`, `isAsset()` - Type checking
- `hasEntity()` - Checks if entity_type and entity_id are set
- `getEntity()` - Loads entity with caching (key: `{entity_type}_{entity_id}`)
- `isActive()` - Checks if current URL matches entity slug

### Caching Strategy

**Cache keys**:
- Menu: `$menu->key` (e.g., "main_menu")
- MenuItem entity: `{entity_type}_{entity_id}` (e.g., "post_123")

**Cache duration**: `config('settings.cache_default')`

**Cache clearing**:
- `MenuEventHandler::onSave()` → calls `$menu->clearCache()` after every menu save
- `Menu::clearCache()` → clears menu cache AND recursively clears all item entity caches
- `MenuItem::clearCache()` → clears only that item's entity cache
- **Important**: Updating a menu item alone does NOT auto-clear cache

**MenuComposer caching**:
```php
$menu = Cache::remember($viewData['key'], config('settings.cache_default'),
    function () use ($viewData) {
        return Menu::where('key', $viewData['key'])
            ->where('status', Menu::ACTIVE)
            ->with('items')
            ->first();
    }
);
```

### Event Handling

**MenuEventHandler** extends `AbstractEventHandler`:
- `onCreating()` - Auto-generates `key` from `name`: `Str::slug($name, '_')`
- `onSave()` - Calls `$menu->clearCache()` after every save

## Coding Conventions

### File Structure
- All PHP files must start with `declare(strict_types=1);`
- Use strict type declarations for all parameters and return types
- Models extend `Bongo\Framework\Models\AbstractModel`
- Controllers extend `AbstractController` (backend) or `AbstractApiController` (api)
- Event handlers extend `AbstractEventHandler`

### Naming Conventions
- Models: Singular (Menu, MenuItem)
- Tables: Plural (menus, menu_items)
- Controllers: Singular + "Controller" (MenuController)
- Requests: Action + Model (StoreMenuRequest, UpdateMenuRequest)
- Resources: Model + "Resource" (MenuResource, MenuItemResource)
- Collections: Model + "Collection" (MenuCollection)
- Route names: `backend.menu.*`, `api.menu.*`, `api.menu_item.*`
- View namespaces: `menu::backend.*`, `menu::frontend.*`

### Return Types
- Controllers returning views: no type hint (returns Illuminate\View\View)
- Controllers returning redirects: `RedirectResponse`
- API controllers: `JsonResponse`, `Response`, specific Resource classes
- Model relationships: specific relation type (`HasMany`, `BelongsTo`)
- Boolean checks: `: bool`
- String methods: `: string`
- Cache methods: `: void`

### Route Patterns
- Backend routes: `/menu`, `/menu/create`, `/menu/{menu}`, `/menu/{menu}/edit`
- API routes: `/menus`, `/menus/{menu}`, `/menu-items`, `/menu-items/{menuItem}`
- Route parameter binding: use model name in singular (Menu $menu, MenuItem $menuItem)

### Database Patterns
- Always use migrations with `Schema::hasTable()` checks
- Foreign keys with cascade: `->onUpdate('cascade')->onDelete('cascade')`
- Enum fields for status/type with model constants
- UUID indexed on all models
- Audit fields: created_by, updated_by, deleted_by (nullable, unsigned integer, indexed)
- Soft deletes on menus only (not menu items - cascade delete instead)

## Common Development Tasks

### Adding a New Linkable Entity Type

1. **Update config/menu.php**:
```php
'entities' => [
    'article' => \App\Models\Article::class,
],
'routes' => [
    'article' => 'frontend.article.show',
],
```

2. **Update MenuEntityType::all()** (if entity is in a package):
```php
if (package()->isEnabled('article')) {
    array_push($categories, 'Article');
}
```

3. **Ensure entity model has**:
- `slug` attribute (used for routing)
- Named route defined in application

### Creating a New Menu

```php
use Bongo\Menu\Models\Menu;

$menu = Menu::create([
    'name' => 'Main Menu',
    // 'key' is auto-generated via MenuEventHandler::onCreating()
    'status' => Menu::ACTIVE,
]);
```

### Adding Menu Items

```php
use Bongo\Menu\Models\MenuItem;

// Internal entity link
$item = MenuItem::create([
    'menu_id' => $menu->id,
    'parent_id' => null,  // Root level
    'name' => 'About Us',
    'entity_type' => 'Page',
    'entity_id' => 123,
    'type' => MenuItem::INTERNAL,
    'target' => MenuItem::SELF,
    'sort_order' => 1,
]);

// External URL
$item = MenuItem::create([
    'menu_id' => $menu->id,
    'parent_id' => null,
    'name' => 'Google',
    'url' => 'https://google.com',
    'type' => MenuItem::EXTERNAL,
    'target' => MenuItem::BLANK,
    'sort_order' => 2,
]);

// Nested item
$child = MenuItem::create([
    'menu_id' => $menu->id,
    'parent_id' => $item->id,  // Parent item
    'name' => 'Sub Page',
    'entity_type' => 'Page',
    'entity_id' => 456,
    'type' => MenuItem::INTERNAL,
    'target' => MenuItem::SELF,
    'sort_order' => 1,
]);
```

### Displaying Menus in Views

```blade
{{-- Include predefined partial with key --}}
@include('menu::frontend.partials.main_menu')

{{-- Or pass custom key --}}
@include('menu::frontend.partials.menu', ['key' => 'custom_menu'])

{{-- MenuComposer will fetch menu by key and inject $menu variable --}}
```

### Manual Menu Rendering

```blade
@if($menu && $menu->hasItems())
    <nav>
        <ul>
            @foreach($menu->items as $item)
                <li class="{{ $item->isActive() ? 'active' : '' }}">
                    <a href="{{ $item->getLink() }}"
                       target="{{ $item->target }}"
                       class="{{ $item->class }}">
                        {{ $item->name }}
                    </a>

                    @if($item->children->count() > 0)
                        <ul>
                            @foreach($item->children as $child)
                                <li>
                                    <a href="{{ $child->getLink() }}">
                                        {{ $child->name }}
                                    </a>
                                </li>
                            @endforeach
                        </ul>
                    @endif
                </li>
            @endforeach
        </ul>
    </nav>
@endif
```

### Clearing Cache Manually

```php
// Clear specific menu
$menu = Menu::where('key', 'main_menu')->first();
$menu->clearCache();  // Clears menu + all nested item entity caches

// Clear specific menu item entity
$menuItem->clearCache();  // Clears only entity cache

// Clear all cache
\Illuminate\Support\Facades\Cache::flush();
```

### Custom Entity Type Discovery

```php
use MenuEntityType;

// Get available entity types based on enabled packages
$types = MenuEntityType::all();
// Returns: ['Document', 'Page', 'Post', 'Post Category', 'Project', 'Project Category']
```

### API Usage Examples

**Fetch menu with items**:
```bash
curl -X GET /api/menus/1 \
  -H "Authorization: Bearer {token}"
```

**Create menu item**:
```bash
curl -X POST /api/menu-items \
  -H "Authorization: Bearer {token}" \
  -H "Content-Type: application/json" \
  -d '{
    "data": {
      "attributes": {
        "menu_id": 1,
        "name": "About",
        "entity_type": "Page",
        "entity_id": 123,
        "type": "internal",
        "target": "_self",
        "sort_order": 1
      }
    }
  }'
```

## Testing

### Test Structure
- All tests extend `Bongo\Menu\Tests\TestCase` (Orchestra Testbench)
- Use `@test` annotation or `test_` prefix
- Tests in `tests/Unit/` and `tests/Feature/`

### Running Tests
```bash
composer test                    # All tests
composer test:coverage          # With coverage (requires Xdebug)
vendor/bin/phpunit --filter test_method_name  # Specific test
```

### Example Test
```php
<?php

declare(strict_types=1);

namespace Bongo\Menu\Tests\Unit;

use Bongo\Menu\Models\Menu;
use Bongo\Menu\Tests\TestCase;

class MenuTest extends TestCase
{
    /** @test */
    public function it_generates_key_on_creation(): void
    {
        $menu = Menu::create(['name' => 'Main Menu']);

        $this->assertEquals('main_menu', $menu->key);
    }
}
```

## Development Commands

```bash
# Testing
composer test                    # Run all tests
composer test:coverage          # Run with coverage
composer test:parallel          # Run tests in parallel

# Code Quality
composer analyse                # PHPStan static analysis (level 1)
composer format                 # Laravel Pint code formatting

# Development Environment (Orchestra Testbench)
composer build                  # Build workbench
composer start                  # Start dev server
composer clear                  # Clear workbench
composer prepare                # Discover packages
```

## Code Quality Standards

### Laravel Pint (pint.json)
```json
{
    "preset": "laravel",
    "rules": {
        "new_with_parentheses": false,
        "nullable_type_declaration_for_default_null_value": false,
        "declare_strict_types": true,
        "declare_parentheses": true
    }
}
```

### PHPStan (phpstan.neon.dist)
- Level: 1 (basic checks)
- Checks: `src/` and `config/`
- Ignores: Eloquent magic methods in Models, Resources, Feature tests

## Troubleshooting

**Menu items not showing**:
- Check menu status is `Menu::ACTIVE`
- Verify `sort_order` is set
- Clear cache: `php artisan cache:clear`

**Links returning '#'**:
- Ensure entity has `slug` attribute
- Verify route name in `config/menu.php`
- Check entity exists and is published
- Review logs for `RouteNotFoundException`

**RouteNotFoundException**:
- Ensure named route exists in application
- Check route name matches config
- Verify entity slug is valid

**Cache not clearing**:
- Menu saves auto-clear via `MenuEventHandler::onSave()`
- Menu item updates do NOT auto-clear - must call `$menu->clearCache()` manually
- Check `settings.cache_default` config value

## Dependencies

- **bongo/framework** ^3.0 - Core framework (AbstractServiceProvider, AbstractModel, AbstractController, traits)
- **Laravel** 10+ or 11+
- **PHP** 8.2+

## Additional Resources

- See `ARCHITECTURE.md` for detailed architecture diagrams and flow charts
- See `README.md` for installation and usage guide
- See `CHANGELOG.md` for version history
