Laravel 13 + Filament 5 admin panel and REST API for a non-profit developer collective, backed by the Pireon OAuth identity server. The public site is a separate, server-rendered application consuming /api/public/v1. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
139 lines
4.8 KiB
PHP
139 lines
4.8 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Http\Controllers\Api\V1;
|
|
|
|
use App\Enums\PageStatus;
|
|
use App\Http\Controllers\Controller;
|
|
use App\Http\Requests\Api\V1\PageRequest;
|
|
use App\Http\Resources\PageResource;
|
|
use App\Models\Page;
|
|
use App\Support\TranslatablePayload;
|
|
use Illuminate\Database\Eloquent\Builder;
|
|
use Illuminate\Http\JsonResponse;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
|
|
|
|
class PageController extends Controller
|
|
{
|
|
/**
|
|
* GET /api/v1/pages
|
|
*
|
|
* Filters: status, category_id, is_featured, search, translated_in, trashed
|
|
* Sorting: sort=-published_at (prefix with "-" for descending)
|
|
*/
|
|
public function index(Request $request): AnonymousResourceCollection
|
|
{
|
|
$this->authorize('viewAny', Page::class);
|
|
|
|
$locale = app()->getLocale();
|
|
|
|
$pages = Page::query()
|
|
->with(['category', 'author'])
|
|
->when($request->filled('status'), fn ($q) => $q->where('status', $request->string('status')))
|
|
->when($request->filled('category_id'), fn ($q) => $q->where('category_id', $request->integer('category_id')))
|
|
->when($request->has('is_featured'), fn ($q) => $q->where('is_featured', $request->boolean('is_featured')))
|
|
// Search inside the active locale's translation rather than across
|
|
// the whole JSON blob, so a Spanish query cannot match English text.
|
|
->when($request->filled('search'), fn ($q) => $q->where(
|
|
fn ($sub) => $sub
|
|
->where("title->{$locale}", 'like', '%'.$request->string('search').'%')
|
|
->orWhere("excerpt->{$locale}", 'like', '%'.$request->string('search').'%')
|
|
))
|
|
->when($request->filled('translated_in'), fn ($q) => $q->translatedIn((string) $request->string('translated_in')))
|
|
->when($request->boolean('trashed'), fn ($q) => $q->onlyTrashed())
|
|
// Callers without the publish permission only ever see live content.
|
|
->unless($request->user()->can('pages.publish'), fn ($q) => $q->published())
|
|
->tap(fn ($q) => $this->applySort($q, $request))
|
|
->paginate(perPage: min($request->integer('per_page', 15), 100))
|
|
->withQueryString();
|
|
|
|
return PageResource::collection($pages);
|
|
}
|
|
|
|
public function store(PageRequest $request): JsonResponse
|
|
{
|
|
$this->authorize('create', Page::class);
|
|
|
|
$page = new Page;
|
|
$page->fill(TranslatablePayload::merge($page, $request->validated(), $page->translatable));
|
|
$page->author_id ??= $request->user()->id;
|
|
|
|
// A page created as published with no explicit date goes live now.
|
|
if ($page->status === PageStatus::Published && $page->published_at === null) {
|
|
$page->published_at = now();
|
|
}
|
|
|
|
$page->save();
|
|
|
|
return PageResource::make($page->load(['category', 'author']))
|
|
->response()
|
|
->setStatusCode(201);
|
|
}
|
|
|
|
public function show(Request $request, Page $page): PageResource
|
|
{
|
|
$this->authorize('view', $page);
|
|
|
|
return PageResource::make($page->load(['category', 'author']));
|
|
}
|
|
|
|
public function update(PageRequest $request, Page $page): PageResource
|
|
{
|
|
$this->authorize('update', $page);
|
|
|
|
$page->fill(TranslatablePayload::merge($page, $request->validated(), $page->translatable));
|
|
|
|
if ($page->status === PageStatus::Published && $page->published_at === null) {
|
|
$page->published_at = now();
|
|
}
|
|
|
|
$page->save();
|
|
|
|
return PageResource::make($page->load(['category', 'author']));
|
|
}
|
|
|
|
public function destroy(Page $page): JsonResponse
|
|
{
|
|
$this->authorize('delete', $page);
|
|
|
|
$page->delete();
|
|
|
|
return response()->json(status: 204);
|
|
}
|
|
|
|
public function restore(Page $page): PageResource
|
|
{
|
|
$this->authorize('restore', $page);
|
|
|
|
$page->restore();
|
|
|
|
return PageResource::make($page->load(['category', 'author']));
|
|
}
|
|
|
|
/**
|
|
* Whitelist of sortable columns. Anything else is ignored rather than
|
|
* passed to the query builder.
|
|
*
|
|
* @param Builder<Page> $query
|
|
*/
|
|
protected function applySort(Builder $query, Request $request): void
|
|
{
|
|
$sort = (string) $request->string('sort', '-created_at');
|
|
$direction = str_starts_with($sort, '-') ? 'desc' : 'asc';
|
|
$column = ltrim($sort, '-');
|
|
|
|
$sortable = ['created_at', 'updated_at', 'published_at', 'position', 'status'];
|
|
|
|
// Sorting by a translatable column targets the active locale's key.
|
|
if ($column === 'title') {
|
|
$query->orderBy('title->'.app()->getLocale(), $direction);
|
|
|
|
return;
|
|
}
|
|
|
|
$query->orderBy(in_array($column, $sortable, true) ? $column : 'created_at', $direction);
|
|
}
|
|
}
|