Files
landing-api/app/Http/Controllers/Api/V1/PageController.php
Josep Piguillem daf6098203 Load a page's image record in the admin API
PageResource only fills `featured_image_media` from the `featuredMedia`
relation when it is loaded, and PageController never loaded it, so every
admin response carried the image's URL with no dimensions, srcset or
placeholder even when the processed record existed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 11:16:31 +02:00

139 lines
4.9 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', 'featuredMedia'])
->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', 'featuredMedia']))
->response()
->setStatusCode(201);
}
public function show(Request $request, Page $page): PageResource
{
$this->authorize('view', $page);
return PageResource::make($page->load(['category', 'author', 'featuredMedia']));
}
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', 'featuredMedia']));
}
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', 'featuredMedia']));
}
/**
* 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);
}
}