Files
landing-api/app/Http/Controllers/Api/V1/PageController.php
Josep Piguillem 3897113313 Initial commit: Pireon Admin — headless admin panel and public API
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>
2026-08-19 17:55:27 +02:00

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);
}
}