Backend Controller
Wncms\Http\Controllers\Backend\BackendController is the base for admin CRUD controllers. It standardizes model resolution, naming, cache-tag handling, and common actions (index/create/store/edit/update/destroy/bulk_delete). Extend it for each backend resource.
Key responsibilities
- Resolve the model class from controller name (
PostController→ model keypost→wncms()->getModelClass('post')). - Derive table / singular / plural names used for views and labels.
- Provide cache tag helpers and a
flush()method. - Offer opinionated CRUD methods with sensible defaults and JSON responses for AJAX.
Properties and defaults
| Property | Type | Source | Default behavior |
|---|---|---|---|
$modelClass | string | getModelClass() | From controller basename without Controller, snake-cased, resolved via wncms()->getModelClass(...). |
$cacheTags | array | getModelCacheTags() | Defaults to [$this->getModelTable()]. |
$singular | string | getModelSingular() | str()->singular($this->getModelTable()). |
$plural | string | getModelPlural() | str()->plural($this->getModelSingular()). |
Override any of these in your child controller as protected properties when needed.
Overridable helpers
// Resolve model class from controller name; override if custom mapping is needed.
public function getModelClass(): string
// Get underlying Eloquent table name.
protected function getModelTable()
// Provide custom cache tags for this resource.
protected function getModelCacheTags(): array
// Customize resource nouns.
protected function getModelSingular(): string
protected function getModelPlural(): string
// Apply current-website list scoping for single/multi website modes.
protected function applyBackendListWebsiteScope(Builder $q, ?Request $request = null, bool $onlyWhenExplicitFilter = false): void
// Resolve website IDs for create/update mutation flows.
protected function resolveBackendMutationWebsiteIds(bool $fallbackToCurrentWhenEmpty = false): array
// Sync website bindings for a created/updated model.
protected function syncBackendMutationWebsites($model, bool $fallbackToCurrentWhenEmpty = false): voidCache control
public function flush(string|array|null $tags = null): bool- Flushes tagged caches via
wncms()->cache()->tags($tag)->flush(). - If
$tagsisnull, uses$this->cacheTags.
Multisite list filtering helper
applyBackendListWebsiteScope() standardizes backend index filtering for models whose website mode is single or multi.
- Reads filter value from request
website_id(or legacywebsite) first. - Falls back to current website ID (
wncms()->website()->get()?->id) when no explicit filter is provided. - Applies model
applyWebsiteScope(...)only when multisite behavior is supported. - No-op for
globalmodels or when no current website is resolved. - For index toolbar filters, standardize request key to
website_idand keepwebsiteas backward-compatible alias when reading request input. - For pages that should show all data by default (for example Posts), pass
trueas third argument so scope is applied only whenwebsite_idis explicitly selected. - Shared website toolbar filter should be shown only when
gss('multi_website')is enabled and model website mode issingleormulti; hide it forglobal.
Example for explicit-filter pages:
$q = $this->modelClass::query();
$this->applyBackendListWebsiteScope($q, $request, true);Multisite mutation helper
For create/update flows, use syncBackendMutationWebsites($model) to keep website bindings compatible across global / single / multi modes.
- For scoped models (
single/multi), it resolves IDs from request keys (website_id,website_ids, and legacy aliases). - For non-admin users, it automatically intersects requested website IDs with websites bound to the current user.
- By default it does not force fallback to current website when IDs are empty.
- If your flow needs fallback, call
syncBackendMutationWebsites($model, true). - For
globalmodels, it no-ops safely. - Backend save flow should not hard-fail when website IDs are empty; models may exist without website bindings.
- In shared backend website selector, create forms may default to
session('selected_website_id')(sidebar website switcher), while edit forms should keep existing bindings and avoid auto-selecting new websites when none are currently bound.
Built-in CRUD actions
All actions assume standard backend Blade paths: backend.{plural}.*.
index(Request $request)- Builds a base query on
$modelClass, orders byid desc, returnsbackend.{plural}.index. - Passes
page_title,models.
- Builds a base query on
create(int|string|null $id = null)- New instance or loads existing for “duplicate/edit-as-new” patterns.
- Returns
backend.{plural}.createwithmodel.
store(Request $request)create($request->all()), then:auto-sync website bindings through
syncBackendMutationWebsites($model).- If AJAX: JSON
{ status, message, redirect }. - Else: redirect to
route('{plural}.edit', ['id' => $model->id]).
- If AJAX: JSON
edit(int|string $id)- Loads model, returns
backend.{plural}.editwithmodel.
- Loads model, returns
update(Request $request, $id)findOrFail-like behavior (returns message if missing),update($request->all()).- auto-sync website bindings through
syncBackendMutationWebsites($model). - If AJAX: JSON
{ status, message, redirect }. - Else: redirect back to edit.
destroy($id)- Deletes model, calls
$this->flush(), redirects to index with success message.
- Deletes model, calls
bulk_delete(Request $request)- Accepts
model_idsas CSV or array, deletes in batch. - If AJAX: JSON with deleted count; else
back()with message.
- Accepts
Messages follow WNCMS translations (e.g.,
__('wncms::word.successfully_updated')). Titles use__('wncms::word.' . $this->singular).
Minimal subclass example
namespace App\Http\Controllers\Backend;
use Illuminate\Http\Request;
use Wncms\Http\Controllers\Backend\BackendController;
class ProductController extends BackendController
{
// Optional: override naming or cache tags
protected array $cacheTags = ['products', 'prices'];
// Optional: customize model mapping if the default name-based resolver isn’t desired
public function getModelClass(): string
{
return wncms()->getModelClass('product'); // explicit
}
// Optional: extend index filters/sorting/pagination
public function index(Request $request)
{
$q = $this->modelClass::query();
$this->applyBackendListWebsiteScope($q);
if ($kw = $request->keyword) {
$q->where(function ($sub) use ($kw) {
$sub->where('name', 'like', "%{$kw}%")
->orWhere('slug', 'like', "%{$kw}%");
});
}
$q->orderByDesc('id');
$models = $q->paginate($request->page_size ?? 50);
return $this->view("backend.{$this->plural}.index", [
'page_title' => __('wncms::word.model_management', ['model_name' => __('wncms::word.' . $this->singular)]),
'models' => $models,
]);
}
}View conventions
- Index:
backend.{plural}.index - Create:
backend.{plural}.create - Edit:
backend.{plural}.edit
The base
Controller::view()delegates towncms()->view(...), so app/theme/package overrides are respected.
Route naming convention
Use plural resource prefixes for route names:
'{plural}.index','{plural}.create','{plural}.edit','{plural}.store','{plural}.update','{plural}.destroy','{plural}.bulk_delete'.
See the Routes section for complete backend route patterns.
Customization tips
- Add validation (e.g., Form Requests) in
store()/update(). - Apply authorization (e.g., policies or middleware) at the route group or controller.
- If you manage files/media/tags (e.g., via Spatie Media Library or Tagify), perform those operations around
store()/update()and call$this->flush()for relevant tags. - For heavy lists, prefer pagination over
get()and add indexes to frequently filtered columns. - For cross-version compatibility, avoid hardcoding legacy foreign-key columns (for example
website_id) when WNCMS multisite relation binding is available.
Manual sort field pattern
If a model has a business sort column (for example sort), make it the default backend index order so order updates are visible immediately after save.
$sort = in_array($request->sort, $this->modelClass::SORTS) ? $request->sort : 'sort';
$direction = in_array($request->direction, ['asc', 'desc']) ? $request->direction : 'desc';
$q->orderBy($sort, $direction);
// Keep deterministic order for identical sort values.
if ($sort !== 'id') {
$q->orderBy('id', 'desc');
}This avoids the common issue where always appending orderBy('id', 'desc') makes manual order adjustments hard to verify in backend lists.
WNCMS multisite compatibility pattern
When the model supports WNCMS multisite methods, resolve website IDs via controller helpers and bind relation with syncModelWebsites(...):
$websiteIds = $this->resolveModelWebsiteIds($this->modelClass);
$model->update([
'name' => $request->name,
'type' => $request->type,
]);
$this->syncModelWebsites($model, $websiteIds);For backend form pages, reuse the common selector partial instead of duplicating website input markup:
@include('wncms::backend.common.website_selector', ['model' => $model, 'websites' => $websites ?? []])When to override vs. extend
- Override
index/create/editto add filters, joins, or eager loads. - Keep
store/update/destroy/bulk_deleteunless your resource has non-standard persistence rules. - Always maintain consistent messages and redirects to ensure a uniform admin UX.