api/app/Http/Controllers/MealPostController.php

273 lines
9.3 KiB
PHP

<?php
namespace App\Http\Controllers;
use App\Http\Requests\MealPostCalendarRequest;
use App\Http\Requests\MealPostsRequest;
use App\Http\Requests\MealPostStatsRequest;
use App\Http\Resources\MealPostsResource;
use App\Models\MealPosts;
use Carbon\CarbonImmutable;
use Carbon\CarbonInterface;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
use Illuminate\Http\Response;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
class MealPostController extends Controller
{
public function index(Request $request): AnonymousResourceCollection
{
$perPage = max(1, min($request->integer('per_page', 15), 100));
$mealPosts = $this->mealPostListQuery($request)
->latest('eaten_at')
->paginate($perPage)
->withQueryString();
return MealPostsResource::collection($mealPosts);
}
public function mine(Request $request): AnonymousResourceCollection
{
$perPage = max(1, min($request->integer('per_page', 15), 100));
$mealPosts = $this->mealPostListQuery($request)
->where('user_id', $request->user()->getKey())
->latest('eaten_at')
->paginate($perPage)
->withQueryString();
return MealPostsResource::collection($mealPosts);
}
public function stats(MealPostStatsRequest $request): JsonResponse
{
$validated = $request->validated();
$period = $validated['period'] ?? 'day';
$referenceDate = CarbonImmutable::createFromFormat('!Y-m-d', $validated['date'] ?? now()->toDateString());
[$startDate, $endDate] = match ($period) {
'week' => [
$referenceDate->startOfWeek(CarbonInterface::MONDAY),
$referenceDate->endOfWeek(CarbonInterface::SUNDAY),
],
default => [
$referenceDate->startOfDay(),
$referenceDate->endOfDay(),
],
};
$mealPosts = MealPosts::query()
->where('user_id', $request->user()->getKey())
->whereBetween('eaten_at', [$startDate, $endDate])
->get(['calories', 'proteins', 'carbs', 'fats', 'eaten_at']);
return response()->json([
'data' => [
'period' => $period,
'date' => $referenceDate->toDateString(),
'startDate' => $startDate->toDateString(),
'endDate' => $endDate->toDateString(),
'totals' => $this->getNutritionTotals($mealPosts),
'days' => $this->getDailyNutritionTotals($mealPosts, $startDate, $endDate),
],
]);
}
public function calendar(MealPostCalendarRequest $request): JsonResponse
{
$month = $request->validated('month') ?? now()->format('Y-m');
$referenceDate = CarbonImmutable::createFromFormat('!Y-m', $month);
$startDate = $referenceDate->startOfMonth();
$endDate = $referenceDate->endOfMonth();
$days = MealPosts::query()
->where('user_id', $request->user()->getKey())
->whereBetween('eaten_at', [$startDate->startOfDay(), $endDate->endOfDay()])
->selectRaw('DATE(eaten_at) as date, COUNT(*) as meals_count')
->groupByRaw('DATE(eaten_at)')
->orderBy('date')
->get()
->map(fn (MealPosts $mealPost): array => [
'date' => CarbonImmutable::parse((string) $mealPost->date)->toDateString(),
'mealsCount' => (int) $mealPost->meals_count,
])
->values();
return response()->json([
'data' => [
'month' => $referenceDate->format('Y-m'),
'startDate' => $startDate->toDateString(),
'endDate' => $endDate->toDateString(),
'days' => $days,
],
]);
}
public function store(MealPostsRequest $request): JsonResponse
{
$data = $this->getValidatedDataWithImageUrl($request);
$ingredients = $data['ingredients'] ?? [];
unset($data['ingredients']);
$mealPost = DB::transaction(function () use ($data, $ingredients, $request): MealPosts {
$mealPost = MealPosts::create([
...$data,
'user_id' => $request->user()->getKey(),
]);
if ($ingredients !== []) {
$mealPost->ingredients()->createMany($ingredients);
}
return $mealPost;
});
$mealPost->setRelation('user', $request->user());
return (new MealPostsResource($this->loadMealPostForResponse($mealPost, $request)))
->response()
->setStatusCode(201);
}
public function show(Request $request, MealPosts $mealPost): MealPostsResource
{
$this->abortIfUnavailable($mealPost);
return new MealPostsResource($this->loadMealPostForResponse($mealPost, $request));
}
public function update(MealPostsRequest $request, MealPosts $mealPost): MealPostsResource
{
$this->abortIfNotOwner($request, $mealPost);
$data = $this->getValidatedDataWithImageUrl($request);
$shouldSyncIngredients = array_key_exists('ingredients', $data);
$ingredients = $data['ingredients'] ?? [];
unset($data['ingredients']);
$mealPost = DB::transaction(function () use ($data, $ingredients, $mealPost, $shouldSyncIngredients): MealPosts {
if ($data !== []) {
$mealPost->update($data);
}
if ($shouldSyncIngredients) {
$mealPost->ingredients()->delete();
if ($ingredients !== []) {
$mealPost->ingredients()->createMany($ingredients);
}
}
return $mealPost->refresh();
});
return new MealPostsResource($this->loadMealPostForResponse($mealPost, $request));
}
public function like(Request $request, MealPosts $mealPost): MealPostsResource
{
$this->abortIfUnavailable($mealPost);
$mealPost->likedByUsers()->syncWithoutDetaching([$request->user()->getKey()]);
return new MealPostsResource($this->loadMealPostForResponse($mealPost, $request));
}
public function unlike(Request $request, MealPosts $mealPost): MealPostsResource
{
$this->abortIfUnavailable($mealPost);
$mealPost->likedByUsers()->detach($request->user()->getKey());
return new MealPostsResource($this->loadMealPostForResponse($mealPost, $request));
}
public function destroy(Request $request, MealPosts $mealPost): Response
{
$this->abortIfNotOwner($request, $mealPost);
$mealPost->delete();
return response()->noContent();
}
private function abortIfNotOwner(Request $request, MealPosts $mealPost): void
{
abort_unless($request->user(), 401);
abort_unless($mealPost->user_id === $request->user()->getKey(), 404);
}
private function abortIfUnavailable(MealPosts $mealPost): void
{
abort_if(
$mealPost->isHidden() || $mealPost->user()->whereNotNull('suspended_at')->exists(),
404,
);
}
private function loadMealPostForResponse(MealPosts $mealPost, Request $request): MealPosts
{
return $mealPost
->loadMissing('user:id,name,avatar_url,account_verified_at', 'ingredients');
}
private function mealPostListQuery(Request $request): Builder
{
return MealPosts::query()
->whereNull('hidden_at')
->whereHas('user', fn (Builder $query): Builder => $query->whereNull('suspended_at'))
->with('user:id,name,avatar_url,account_verified_at');
}
private function getValidatedDataWithImageUrl(MealPostsRequest $request): array
{
$data = $request->validated();
if ($request->hasFile('image')) {
$path = $request->file('image')->store('meal-posts', 'public');
$data['image_url'] = $path;
}
unset($data['image']);
return $data;
}
private function getDailyNutritionTotals(Collection $mealPosts, CarbonImmutable $startDate, CarbonImmutable $endDate): array
{
$days = [];
for ($date = $startDate; $date->lte($endDate); $date = $date->addDay()) {
$day = $date->toDateString();
$dayMealPosts = $mealPosts->filter(
fn (MealPosts $mealPost): bool => $mealPost->eaten_at->toDateString() === $day
);
$days[] = [
'date' => $day,
'totals' => $this->getNutritionTotals($dayMealPosts),
];
}
return $days;
}
private function getNutritionTotals(Collection $mealPosts): array
{
return [
'calories' => (int) $mealPosts->sum(fn (MealPosts $mealPost): int => (int) ($mealPost->calories ?? 0)),
'proteins' => round((float) $mealPosts->sum(fn (MealPosts $mealPost): float => (float) ($mealPost->proteins ?? 0)), 2),
'carbs' => round((float) $mealPosts->sum(fn (MealPosts $mealPost): float => (float) ($mealPost->carbs ?? 0)), 2),
'fats' => round((float) $mealPosts->sum(fn (MealPosts $mealPost): float => (float) ($mealPost->fats ?? 0)), 2),
'mealsCount' => $mealPosts->count(),
];
}
}