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.
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:
// 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:
php artisan make:controller Shopper/PostController
This creates a controller in app/Http/Controllers/Shopper/PostController.php:
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:
// 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:
// config/shopper/admin.php
'pages' => [
'namespace' => 'App\\Livewire\\Shopper',
'view_path' => resource_path('views/livewire/shopper'),
],
Create a Livewire component:
php artisan make:livewire Shopper/PostIndex
Use Shopper’s layout attribute to inherit the admin panel styling:
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:
// routes/shopper.php
use App\Livewire\Shopper\PostIndex;
Route::get('/posts', PostIndex::class)->name('shopper.posts.index');
Use #[Layout('shopper::components.layouts.app')] on your Livewire component to inherit the admin panel layout with sidebar, header, and notification system.
Adding Navigation
To add your custom page to the admin sidebar, create a sidebar extension. See the 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:
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:
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:
use Livewire\Attributes\Locked;
use Shopper\Models\Order;
#[Locked]
public Order $order;
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:
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:
public function togglePermission(int $permissionId): void
{
$this->authorize('access_setting');
$permission = Permission::query()->find($permissionId);
if ($permission === null) {
return;
}
// ...
}