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