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 $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') ?? []); } } }