Modules

Modules extend Textstem with new content types that don't fit pages or posts, such as subscribers, shows or products. A module is an admin section of its own: a model, a controller, a policy, a Livewire table and admin views, all wrapped in the Textstem admin layout.

The quickest start is php artisan textstem:make:module; see Creating Modules with the Console. This page explains the parts, using a 'subscriber module' as the example.

Routes

Put the module's routes under the /textstem prefix with the same middleware as the package's own admin routes. textstem.admin checks the access-textstem-admin gate, so only people allowed into the admin reach them:

// routes/web.php
use App\Http\Controllers\SubscriberController;

Route::prefix('/textstem')
    ->middleware(['web', 'auth:sanctum', 'verified', 'textstem.admin'])
    ->group(function () {
        Route::resource('subscribers', SubscriberController::class);
    });

Permissions and the policy

Textstem uses Spatie's laravel-permission. Create the permissions your policy checks, for example list subscribers, view subscribers, create subscribers, update subscribers and delete subscribers, with a seeder or in Admin → Roles.

Then register the policy. The package replaces Laravel's policy guessing for every model, so App\Policies\SubscriberPolicy is not found on its own:

// AppServiceProvider::boot()
use Illuminate\Support\Facades\Gate;

Gate::policy(\App\Models\Subscriber::class, \App\Policies\SubscriberPolicy::class);

Without this line, every check on the model denies everyone except super-admins. See Authorisation.

Controller

The controller is an ordinary resource controller:

<?php

namespace App\Http\Controllers;

use App\Models\Subscriber;
use Illuminate\Http\Request;
use Illuminate\View\View;

class SubscriberController extends Controller
{
    public function index(Request $request): View
    {
        $this->authorize('viewAny', Subscriber::class);

        return view('admin.subscribers.index');
    }

    // create, store, edit, update, destroy ...
}

Use policy method names (viewAny, create, update, delete) in authorize(), so the calls reach the policy.

Index View

The index view wraps its content in <x-textstem::app-layout> to match the rest of the admin:

<x-textstem::app-layout>
    <x-slot name="header">
        <h2 class="h2">Subscribers</h2>
    </x-slot>
    <x-slot name="topcontrols">
        @can('create', App\Models\Subscriber::class)
            <a href="{{ route('subscribers.create') }}" class="button button-primary float-right">
                <x-ts::icons.add class=""/>
                {{ __('Create') }}
            </a>
        @endcan
    </x-slot>
    <livewire:tables.subscriber-table />
</x-textstem::app-layout>

The list is a Livewire table built on Textstem's Table component:

<?php

namespace App\Livewire\Tables;

use App\Models\Subscriber;
use Illuminate\Database\Eloquent\Builder;
use Medialight\Textstem\Livewire\Table;
use Medialight\Textstem\Table\Column;

class SubscriberTable extends Table
{
    public string $resourceName = 'subscribers';

    public function query(): Builder
    {
        return Subscriber::query();
    }

    public function columns(): array
    {
        return [
            Column::make('email', 'Email'),
            Column::make('source', 'Source'),
            Column::make('updated_at', 'Modified')->component('columns.formatters.modified'),
            Column::make('', '')->component('columns.formatters.editbuttons')->notsortable(),
        ];
    }
}

Adding it to the admin menu

Add the module to the modules key of config/textstemapp.php. The key isn't in the published file, so add it yourself:

'modules' => [
    [
        'label' => 'Subscribers',
        'route' => 'subscribers.index',
        'access' => 'list subscribers',
    ],
],

route is a route name (use path for a literal URL instead), and access is checked with auth()->user()->can(...). Entries go in a "Modules" group; add 'group' => '...' to change it, or 'children' => [...] for a submenu of entries with the same shape.

esc