Tags are named in every language and made from the page form; the byline is the member who signs a post on the site. The public API sends both, filters posts by tag and lists the tags in use. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
150 lines
5.2 KiB
PHP
150 lines
5.2 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
|
|
{
|
|
protected const WITH = ['category', 'author', 'featuredMedia', 'tags'];
|
|
|
|
/**
|
|
* 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(self::WITH)
|
|
->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, collect($request->validated())->except('tag_ids')->all(), $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();
|
|
$this->syncTags($page, $request);
|
|
|
|
return PageResource::make($page->load(self::WITH))
|
|
->response()
|
|
->setStatusCode(201);
|
|
}
|
|
|
|
public function show(Request $request, Page $page): PageResource
|
|
{
|
|
$this->authorize('view', $page);
|
|
|
|
return PageResource::make($page->load(self::WITH));
|
|
}
|
|
|
|
public function update(PageRequest $request, Page $page): PageResource
|
|
{
|
|
$this->authorize('update', $page);
|
|
|
|
$page->fill(TranslatablePayload::merge($page, collect($request->validated())->except('tag_ids')->all(), $page->translatable));
|
|
|
|
if ($page->status === PageStatus::Published && $page->published_at === null) {
|
|
$page->published_at = now();
|
|
}
|
|
|
|
$page->save();
|
|
$this->syncTags($page, $request);
|
|
|
|
return PageResource::make($page->load(self::WITH));
|
|
}
|
|
|
|
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(self::WITH));
|
|
}
|
|
|
|
/**
|
|
* 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);
|
|
}
|
|
|
|
protected function syncTags(Page $page, PageRequest $request): void
|
|
{
|
|
if ($request->has('tag_ids')) {
|
|
$page->tags()->sync($request->validated('tag_ids') ?? []);
|
|
}
|
|
}
|
|
}
|