Files
ABSDEV/Modules/Pages/Http/Controllers/PageController.php
T
Mark Gregory b738ff908b Professional searchable page library.
* Clear Live, Draft and Homepage indicators.
* Direct Edit page, Edit content and View page actions.
* Page editor divided into:
    * Page details
    * SEO and URL
    * Publishing
* Automatic, unique URL slugs.
* Publish and expiry scheduling.
* Page templates: Default, Landing and Full Width.
* Homepage replacement handled automatically.
* Current homepage protected from accidental deletion.
* Page duplication includes its complete content structure.
* Content composer now works from the actual page record and page ID.
* Clearer section-based terminology for editors.
* Reusable content remains available as an advanced content library rather than dominating the workflow.

The package contains the eight complete changed files, preserving their project paths. It is not a patch collection.
2026-07-28 13:35:06 +01:00

152 lines
5.8 KiB
PHP

<?php
namespace Modules\Pages\Http\Controllers;
use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
use Illuminate\Validation\Rule;
use Illuminate\View\View;
use Modules\Content\Models\PageContent;
use Modules\Pages\Models\Page;
final class PageController extends Controller
{
public function index(Request $request): View
{
$records = Page::query()
->withCount('contentPlacements')
->when($request->string('search')->isNotEmpty(), function ($query) use ($request): void {
$search = $request->string('search')->toString();
$query->where(fn ($page) => $page
->where('title', 'like', "%{$search}%")
->orWhere('slug', 'like', "%{$search}%")
->orWhere('summary', 'like', "%{$search}%"));
})
->when($request->string('status')->isNotEmpty(), fn ($query) => $query->where('status', $request->string('status')->toString()))
->orderByDesc('is_home')
->orderBy('title')
->paginate(24)
->withQueryString();
return view('pages::admin.index', compact('records'));
}
public function create(): View
{
return view('pages::admin.form', ['record' => new Page(['status' => 'draft', 'template' => 'default'])]);
}
public function store(Request $request): RedirectResponse
{
$data = $this->validated($request);
$data['slug'] = $this->uniqueSlug($data['slug'] ?: $data['title']);
$data['is_home'] = $request->boolean('is_home');
$data['published_at'] = $data['status'] === 'published' ? ($data['published_at'] ?? now()) : null;
$page = Page::query()->create($data);
$this->enforceSingleHomepage($page);
return redirect()->route('administration.pages.edit', $page)->with('status', 'Page created. Add content sections when you are ready.');
}
public function edit(Page $page): View
{
$page->loadCount('contentPlacements');
return view('pages::admin.form', ['record' => $page]);
}
public function update(Request $request, Page $page): RedirectResponse
{
$oldSlug = $page->slug;
$data = $this->validated($request, $page);
$data['slug'] = $this->uniqueSlug($data['slug'] ?: $data['title'], $page);
$data['is_home'] = $request->boolean('is_home');
$data['published_at'] = $data['status'] === 'published' ? ($data['published_at'] ?? $page->published_at ?? now()) : null;
$page->update($data);
$this->enforceSingleHomepage($page);
if ($oldSlug !== $page->slug) {
PageContent::query()->where('page_id', $page->id)->orWhere('page_key', $oldSlug)->update(['page_key' => $page->slug, 'page_id' => $page->id]);
}
return back()->with('status', 'Page saved.');
}
public function duplicate(Request $request, Page $page): RedirectResponse
{
$copy = $page->replicate(['slug', 'status', 'published_at', 'preview_token', 'locked_by', 'locked_at', 'is_home']);
$copy->title = $page->title.' copy';
$copy->slug = $this->uniqueSlug($copy->title);
$copy->status = 'draft';
$copy->published_at = null;
$copy->is_home = false;
$copy->save();
foreach ($page->contentPlacements()->orderBy('region')->orderBy('sort_order')->get() as $placement) {
$copy->contentPlacements()->create([
'page_key' => $copy->slug,
'content_document_id' => $placement->content_document_id,
'region' => $placement->region,
'sort_order' => $placement->sort_order,
'is_enabled' => $placement->is_enabled,
'visibility_rules' => $placement->visibility_rules,
'presentation' => $placement->presentation,
'starts_at' => $placement->starts_at,
'ends_at' => $placement->ends_at,
]);
}
return redirect()->route('administration.pages.edit', $copy)->with('status', 'Draft copy created with the same content structure.');
}
public function destroy(Page $page): RedirectResponse
{
if ($page->is_home) {
return back()->withErrors(['page' => 'Choose another homepage before deleting the current homepage.']);
}
$page->delete();
return redirect()->route('administration.pages.index')->with('status', 'Page moved to the bin.');
}
private function validated(Request $request, ?Page $page = null): array
{
return $request->validate([
'title' => ['required', 'string', 'max:255'],
'slug' => ['nullable', 'string', 'max:255', Rule::unique('pages', 'slug')->ignore($page?->id)->whereNull('deleted_at')],
'summary' => ['nullable', 'string', 'max:1000'],
'content' => ['nullable', 'string'],
'status' => ['required', Rule::in(['draft', 'published'])],
'template' => ['required', Rule::in(['default', 'landing', 'full-width'])],
'published_at' => ['nullable', 'date'],
'expires_at' => ['nullable', 'date', 'after:published_at'],
'is_home' => ['sometimes', 'boolean'],
]);
}
private function uniqueSlug(string $value, ?Page $except = null): string
{
$base = Str::slug($value) ?: 'page';
$slug = $base;
$suffix = 2;
while (Page::withTrashed()->where('slug', $slug)->when($except, fn ($query) => $query->whereKeyNot($except->getKey()))->exists()) {
$slug = $base.'-'.$suffix++;
}
return $slug;
}
private function enforceSingleHomepage(Page $page): void
{
if ($page->is_home) {
Page::query()->whereKeyNot($page->id)->where('is_home', true)->update(['is_home' => false]);
}
}
}