Skip to content

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 key postwncms()->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

PropertyTypeSourceDefault behavior
$modelClassstringgetModelClass()From controller basename without Controller, snake-cased, resolved via wncms()->getModelClass(...).
$cacheTagsarraygetModelCacheTags()Defaults to [$this->getModelTable()].
$singularstringgetModelSingular()str()->singular($this->getModelTable()).
$pluralstringgetModelPlural()str()->plural($this->getModelSingular()).

Override any of these in your child controller as protected properties when needed.

Overridable helpers

php
// 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): void

Cache control

php
public function flush(string|array|null $tags = null): bool
  • Flushes tagged caches via wncms()->cache()->tags($tag)->flush().
  • If $tags is null, 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 legacy website) 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 global models or when no current website is resolved.
  • For index toolbar filters, standardize request key to website_id and keep website as backward-compatible alias when reading request input.
  • For pages that should show all data by default (for example Posts), pass true as third argument so scope is applied only when website_id is explicitly selected.
  • Shared website toolbar filter should be shown only when gss('multi_website') is enabled and model website mode is single or multi; hide it for global.

Example for explicit-filter pages:

php
$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 global models, 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 by id desc, returns backend.{plural}.index.
    • Passes page_title, models.
  • create(int|string|null $id = null)

    • New instance or loads existing for “duplicate/edit-as-new” patterns.
    • Returns backend.{plural}.create with model.
  • 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]).
  • edit(int|string $id)

    • Loads model, returns backend.{plural}.edit with model.
  • 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.
  • bulk_delete(Request $request)

    • Accepts model_ids as CSV or array, deletes in batch.
    • If AJAX: JSON with deleted count; else back() with message.

Messages follow WNCMS translations (e.g., __('wncms::word.successfully_updated')). Titles use __('wncms::word.' . $this->singular).

Minimal subclass example

php
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 to wncms()->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.

php
$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(...):

php
$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:

blade
@include('wncms::backend.common.website_selector', ['model' => $model, 'websites' => $websites ?? []])

When to override vs. extend

  • Override index/create/edit to add filters, joins, or eager loads.
  • Keep store/update/destroy/bulk_delete unless your resource has non-standard persistence rules.
  • Always maintain consistent messages and redirects to ensure a uniform admin UX.

Built with ❤️ for WNCMS