> ## Documentation Index
> Fetch the complete documentation index at: https://docs.laravelshopper.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Controllers

> Add custom pages to the admin panel using controllers or Livewire components.

You can add custom pages to the Shopper admin panel using either traditional Laravel controllers or Livewire components. Both approaches share the same authentication middleware and can be integrated into the admin sidebar navigation.

## Custom Routes

To add custom routes inside the admin panel, set the `custom_file` key in `config/shopper/routes.php` to point to a dedicated route file:

```php theme={null}
// config/shopper/routes.php

'custom_file' => base_path('routes/shopper.php'),
```

Routes defined in this file are automatically loaded with the admin panel's middleware stack (authentication, authorization).

## Using Controllers

You can use standard Laravel controllers with Shopper. Create a controller in any namespace you prefer:

```bash theme={null}
php artisan make:controller Shopper/PostController
```

This creates a controller in `app/Http/Controllers/Shopper/PostController.php`:

```php theme={null}
namespace App\Http\Controllers\Shopper;

use App\Http\Controllers\Controller;
use Illuminate\Contracts\View\View;

class PostController extends Controller
{
    public function index(): View
    {
        $posts = Post::query()->latest()->paginate(20);

        return view('shopper.posts.index', compact('posts'));
    }

    public function create(): View
    {
        return view('shopper.posts.create');
    }

    public function store(Request $request)
    {
        // Handle form submission

        return redirect()->route('shopper.posts.index');
    }
}
```

Register the routes in your custom route file:

```php theme={null}
// routes/shopper.php

use App\Http\Controllers\Shopper\PostController;

Route::get('/posts', [PostController::class, 'index'])->name('shopper.posts.index');
Route::get('/posts/create', [PostController::class, 'create'])->name('shopper.posts.create');
Route::post('/posts', [PostController::class, 'store'])->name('shopper.posts.store');
```

## Using Livewire Components

Livewire is the recommended approach since the entire Shopper admin is built with Livewire. The `pages` configuration in `config/shopper/admin.php` controls the namespace and view path for custom Livewire components:

```php theme={null}
// config/shopper/admin.php

'pages' => [
    'namespace' => 'App\\Livewire\\Shopper',
    'view_path' => resource_path('views/livewire/shopper'),
],
```

Create a Livewire component:

```bash theme={null}
php artisan make:livewire Shopper/PostIndex
```

Use Shopper's layout attribute to inherit the admin panel styling:

```php theme={null}
namespace App\Livewire\Shopper;

use Illuminate\Contracts\View\View;
use Livewire\Attributes\Layout;
use Livewire\Component;

#[Layout('shopper::components.layouts.app')]
class PostIndex extends Component
{
    public function render(): View
    {
        return view('livewire.shopper.post-index');
    }
}
```

Register Livewire routes in your custom route file:

```php theme={null}
// routes/shopper.php

use App\Livewire\Shopper\PostIndex;

Route::get('/posts', PostIndex::class)->name('shopper.posts.index');
```

<Tip>
  Use `#[Layout('shopper::components.layouts.app')]` on your Livewire component to inherit the admin panel layout with sidebar, header, and notification system.
</Tip>

## Adding Navigation

To add your custom page to the admin sidebar, create a sidebar extension. See the [Navigation](/v2/extending/navigation) page for details on adding menu items that link to your controller or Livewire routes.

## Authorization Pattern for Custom Admin Components

Custom Livewire components that extend or mimic Shopper's admin surfaces (orders, products, customers, settings) must follow the authorization pattern that Shopper enforces internally. Skipping any of these steps reintroduces the bypass surface that v2.8 closed across the shipped components.

### Chain `authorize()` on Filament Actions

Declare the required ability on the Filament `Action` itself, not inside the closure. This hides the button when the user lacks the ability instead of throwing a 403 on click:

```php theme={null}
use Filament\Actions\Action;

Action::make('cancel')
    ->authorize('edit_orders')
    ->action(function (): void {
        // ...
    });
```

### Authorize Livewire Method Handlers

Every public Livewire method that mutates state must call `$this->authorize()` on its first line. Livewire methods are reachable from the browser regardless of UI state, so the check belongs in the method body, not in the view that rendered the trigger:

```php theme={null}
public function leaveNotes(): void
{
    $this->authorize('edit_orders');

    // ...
}
```

### Lock Public Model Properties

Any `public Model $x` property on a Livewire component must be marked `#[Locked]`. Without it, the browser can rebind the property to a different model ID and bypass authorization that was performed on the original target:

```php theme={null}
use Livewire\Attributes\Locked;
use Shopper\Models\Order;

#[Locked]
public Order $order;
```

### Allow-List User Input

When persisting user input from the browser, use an explicit `Arr::only(...)` allow-list. `Arr::except(...)` is fragile: any new field that lands in the payload (including private state added by the framework) silently flows into the database:

```php theme={null}
use Illuminate\Support\Arr;

$data = Arr::only($payload, ['first_name', 'last_name', 'email']);
```

### Guard Lookups Against Null

Livewire components that resolve a model from user input must handle the lookup failure path. Returning early on null avoids the cascade of bypasses that follow when a misbound ID is treated as authoritative:

```php theme={null}
public function togglePermission(int $permissionId): void
{
    $this->authorize('access_setting');

    $permission = Permission::query()->find($permissionId);

    if ($permission === null) {
        return;
    }

    // ...
}
```
