310 lines
9.5 KiB
PHP
310 lines
9.5 KiB
PHP
<?php
|
||
|
||
namespace App\Http\Controllers;
|
||
|
||
use App\Models\NewsPost;
|
||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||
use Illuminate\Foundation\Validation\ValidatesRequests;
|
||
use Illuminate\Http\JsonResponse;
|
||
use Illuminate\Http\Request;
|
||
use Illuminate\Routing\Controller as BaseController;
|
||
use Illuminate\Support\Str;
|
||
use Illuminate\Validation\Rule;
|
||
|
||
class NewsPostController extends BaseController
|
||
{
|
||
use AuthorizesRequests, ValidatesRequests;
|
||
|
||
public function __construct()
|
||
{
|
||
// zápis jen pro přihlášené (admin policy vyřešíš přes Policy)
|
||
$this->middleware('auth:sanctum')->only(['store', 'update', 'destroy']);
|
||
}
|
||
|
||
/**
|
||
* Display a listing of the resource.
|
||
*
|
||
* Podporuje volitelný dotazový parametr ?lang=cs / ?lang=en
|
||
* Pokud je lang zadán, title/content/excerpt budou vráceny jen v daném jazyce.
|
||
*/
|
||
public function index(Request $request): JsonResponse
|
||
{
|
||
$perPage = (int) $request->get('per_page', 10);
|
||
$limit = (int) $request->get('limit', 0);
|
||
$includeUnpublished = $request->boolean('include_unpublished', false);
|
||
|
||
// volitelný jazyk – pokud není, použije se app locale
|
||
$lang = $request->query('lang');
|
||
if (! is_string($lang) || $lang === '') {
|
||
$lang = app()->getLocale();
|
||
}
|
||
|
||
$query = NewsPost::query()
|
||
->orderByDesc('published_at');
|
||
|
||
if (! $includeUnpublished) {
|
||
$query->where('is_published', true)
|
||
->whereNotNull('published_at')
|
||
->where('published_at', '<=', now());
|
||
}
|
||
|
||
if ($limit > 0) {
|
||
$items = $query->limit($limit)->get();
|
||
} else {
|
||
$items = $query->paginate($perPage);
|
||
}
|
||
|
||
$mapTranslation = function (NewsPost $post) use ($lang) {
|
||
$data = $post->toArray();
|
||
|
||
// getTranslation(attr, lang, useFallback=true)
|
||
$data['title'] = $post->getTranslation('title', $lang, true);
|
||
$data['content'] = $post->getTranslation('content', $lang, true);
|
||
$data['excerpt'] = $post->getTranslation('excerpt', $lang, true);
|
||
|
||
return $data;
|
||
};
|
||
|
||
if ($limit > 0) {
|
||
$items = $items->map($mapTranslation);
|
||
return response()->json($items);
|
||
}
|
||
|
||
$items->getCollection()->transform($mapTranslation);
|
||
|
||
return response()->json($items);
|
||
}
|
||
|
||
/**
|
||
* Detail novinky (přes slug).
|
||
* Public – ale jen pokud je publikovaná, jinak 404.
|
||
*/
|
||
public function show(NewsPost $news): JsonResponse
|
||
{
|
||
if (
|
||
! $news->is_published ||
|
||
! $news->published_at ||
|
||
$news->published_at->isFuture()
|
||
) {
|
||
abort(404);
|
||
}
|
||
|
||
return response()->json($news);
|
||
}
|
||
|
||
/**
|
||
* Vytvoření novinky (admin).
|
||
*/
|
||
public function store(Request $request): JsonResponse
|
||
{
|
||
$this->authorize('create', NewsPost::class);
|
||
|
||
$data = $this->validateData($request);
|
||
|
||
if (empty($data['slug'])) {
|
||
$data['slug'] = $this->makeSlugFromTitle($data['title'] ?? null);
|
||
}
|
||
|
||
if (! empty($data['is_published']) && empty($data['published_at'])) {
|
||
$data['published_at'] = now();
|
||
}
|
||
|
||
$data['author_id'] = $request->user()?->id;
|
||
|
||
$news = NewsPost::create($data);
|
||
|
||
return response()->json($news, 201);
|
||
}
|
||
|
||
/**
|
||
* Aktualizace novinky (admin).
|
||
*/
|
||
public function update(Request $request, NewsPost $news): JsonResponse
|
||
{
|
||
$this->authorize('update', $news);
|
||
|
||
$data = $this->validateData($request, partial: true);
|
||
|
||
// pokud přišla změna title a není explicitně zadaný slug, dopočítej ho
|
||
if (
|
||
array_key_exists('title', $data) &&
|
||
(! array_key_exists('slug', $data) || empty($data['slug']))
|
||
) {
|
||
$generated = $this->makeSlugFromTitle($data['title']);
|
||
if ($generated !== null) {
|
||
$data['slug'] = $generated;
|
||
}
|
||
}
|
||
|
||
if (
|
||
array_key_exists('is_published', $data) &&
|
||
$data['is_published'] &&
|
||
empty($data['published_at'])
|
||
) {
|
||
$data['published_at'] = $news->published_at ?? now();
|
||
}
|
||
|
||
$news->fill($data);
|
||
$news->save();
|
||
|
||
return response()->json($news);
|
||
}
|
||
|
||
/**
|
||
* Smazání novinky (admin).
|
||
*/
|
||
public function destroy(NewsPost $news): JsonResponse
|
||
{
|
||
$this->authorize('delete', $news);
|
||
|
||
$news->delete();
|
||
|
||
return response()->json(null, 204);
|
||
}
|
||
|
||
/**
|
||
* Validace dat.
|
||
*
|
||
* Podporuje:
|
||
* - string hodnoty (jednotlivý překlad pro aktuální locale)
|
||
* - pole překladů: { "cs": "...", "en": "..." }
|
||
*/
|
||
protected function validateData(Request $request, bool $partial = false): array
|
||
{
|
||
$required = $partial ? 'sometimes' : 'required';
|
||
|
||
/** @var NewsPost|null $routeNews */
|
||
$routeNews = $request->route('news'); // může být null, nebo model díky route model bindingu
|
||
|
||
$rules = [
|
||
'title' => [
|
||
$required,
|
||
function (string $attribute, $value, \Closure $fail) {
|
||
if (is_string($value)) {
|
||
if (mb_strlen($value) > 255) {
|
||
$fail('The '.$attribute.' may not be greater than 255 characters.');
|
||
}
|
||
return;
|
||
}
|
||
|
||
if (is_array($value)) {
|
||
foreach ($value as $locale => $text) {
|
||
if (! is_string($text)) {
|
||
$fail("The {$attribute}.{$locale} must be a string.");
|
||
return;
|
||
}
|
||
if (mb_strlen($text) > 255) {
|
||
$fail("The {$attribute}.{$locale} may not be greater than 255 characters.");
|
||
return;
|
||
}
|
||
}
|
||
return;
|
||
}
|
||
|
||
$fail('The '.$attribute.' must be a string or an array of translated strings.');
|
||
},
|
||
],
|
||
|
||
'slug' => [
|
||
'sometimes',
|
||
'nullable',
|
||
'string',
|
||
'max:255',
|
||
Rule::unique('news_posts', 'slug')->ignore($routeNews?->getKey()),
|
||
],
|
||
|
||
'content' => [
|
||
$required,
|
||
function (string $attribute, $value, \Closure $fail) {
|
||
if (is_string($value)) {
|
||
return;
|
||
}
|
||
|
||
if (is_array($value)) {
|
||
foreach ($value as $locale => $text) {
|
||
if (! is_string($text)) {
|
||
$fail("The {$attribute}.{$locale} must be a string.");
|
||
return;
|
||
}
|
||
}
|
||
return;
|
||
}
|
||
|
||
$fail('The '.$attribute.' must be a string or an array of translated strings.');
|
||
},
|
||
],
|
||
|
||
'excerpt' => [
|
||
'sometimes',
|
||
function (string $attribute, $value, \Closure $fail) {
|
||
if ($value === null) {
|
||
return;
|
||
}
|
||
|
||
if (is_string($value)) {
|
||
if (mb_strlen($value) > 500) {
|
||
$fail('The '.$attribute.' may not be greater than 500 characters.');
|
||
}
|
||
return;
|
||
}
|
||
|
||
if (is_array($value)) {
|
||
foreach ($value as $locale => $text) {
|
||
if (! is_string($text)) {
|
||
$fail("The {$attribute}.{$locale} must be a string.");
|
||
return;
|
||
}
|
||
if (mb_strlen($text) > 500) {
|
||
$fail("The {$attribute}.{$locale} may not be greater than 500 characters.");
|
||
return;
|
||
}
|
||
}
|
||
return;
|
||
}
|
||
|
||
$fail('The '.$attribute.' must be null, a string, or an array of translated strings.');
|
||
},
|
||
],
|
||
|
||
'is_published' => ['sometimes', 'boolean'],
|
||
'published_at' => ['sometimes', 'nullable', 'date'],
|
||
];
|
||
|
||
return $request->validate($rules);
|
||
}
|
||
|
||
/**
|
||
* Vytvoří slug z titulku – umí pracovat jak se stringem, tak s polem překladů.
|
||
*
|
||
* - pokud je $title string → slug z něj
|
||
* - pokud je $title array → použije se:
|
||
* title[aktuální_locale] || title['en'] || první dostupná hodnota
|
||
*/
|
||
protected function makeSlugFromTitle(string|array|null $title): ?string
|
||
{
|
||
if ($title === null) {
|
||
return null;
|
||
}
|
||
|
||
if (is_array($title)) {
|
||
$locale = app()->getLocale();
|
||
|
||
$base = $title[$locale]
|
||
?? $title['en']
|
||
?? reset($title);
|
||
|
||
if (! is_string($base) || $base === '') {
|
||
return null;
|
||
}
|
||
|
||
return Str::slug($base);
|
||
}
|
||
|
||
if ($title === '') {
|
||
return null;
|
||
}
|
||
|
||
return Str::slug($title);
|
||
}
|
||
}
|