294 lines
8.8 KiB
PHP
294 lines
8.8 KiB
PHP
<?php
|
|||
|
|
|
||
|
|
namespace App\Services;
|
||
|
|
|
||
|
|
use App\Dtos\RecipeDto;
|
||
|
|
use App\Models\Ingredient;
|
||
|
|
use App\Models\Recipe;
|
||
|
|
use App\Models\RecipeImage;
|
||
|
|
use App\Models\RecipeNutrition;
|
||
|
|
use App\Models\RecipeStep;
|
||
|
|
use App\Models\Tag;
|
||
|
|
use Illuminate\Support\Facades\DB;
|
||
|
|
use Illuminate\Support\Str;
|
||
|
|
|
||
|
|
class RecipeIngestService
|
||
|
|
{
|
||
|
|
/**
|
||
|
|
* Ingest a recipe from DTO - create or update
|
||
|
|
*
|
||
|
|
* @param RecipeDto $recipeDto
|
||
|
|
* @return Recipe
|
||
|
|
*/
|
||
|
|
public function ingest(RecipeDto $recipeDto): Recipe
|
||
|
|
{
|
||
|
|
return DB::transaction(function () use ($recipeDto) {
|
||
|
|
// Find existing recipe by slug or create new
|
||
|
|
$recipe = Recipe::where('slug', $recipeDto->slug)->first();
|
||
|
|
|
||
|
|
if ($recipe) {
|
||
|
|
// Update existing recipe
|
||
|
|
$this->updateRecipe($recipe, $recipeDto);
|
||
|
|
} else {
|
||
|
|
// Create new recipe
|
||
|
|
$recipe = $this->createRecipe($recipeDto);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Sync related data
|
||
|
|
$this->syncIngredients($recipe, $recipeDto);
|
||
|
|
$this->syncSteps($recipe, $recipeDto);
|
||
|
|
$this->syncImages($recipe, $recipeDto);
|
||
|
|
$this->syncNutrition($recipe, $recipeDto);
|
||
|
|
$this->syncTags($recipe, $recipeDto);
|
||
|
|
|
||
|
|
return $recipe->load(['ingredients', 'steps', 'images', 'nutrition', 'tags']);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Create a new recipe
|
||
|
|
*
|
||
|
|
* @param RecipeDto $dto
|
||
|
|
* @return Recipe
|
||
|
|
*/
|
||
|
|
protected function createRecipe(RecipeDto $dto): Recipe
|
||
|
|
{
|
||
|
|
return Recipe::create([
|
||
|
|
'title' => $dto->title,
|
||
|
|
'slug' => $dto->slug,
|
||
|
|
'description' => $dto->description,
|
||
|
|
'prep_time' => $dto->prepTime,
|
||
|
|
'cook_time' => $dto->cookTime,
|
||
|
|
'total_time' => $dto->totalTime,
|
||
|
|
'servings' => $dto->servings,
|
||
|
|
'difficulty' => $dto->difficulty,
|
||
|
|
'cuisine' => $dto->cuisine,
|
||
|
|
'category' => $dto->category,
|
||
|
|
'calories' => $dto->calories,
|
||
|
|
'featured_image' => $dto->featuredImage,
|
||
|
|
'video_url' => $dto->videoUrl,
|
||
|
|
'status' => $dto->status ?? 0,
|
||
|
|
'views_count' => $dto->viewsCount ?? 0,
|
||
|
|
'rating_avg' => $dto->ratingAvg,
|
||
|
|
'rating_count' => $dto->ratingCount ?? 0,
|
||
|
|
]);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Update an existing recipe
|
||
|
|
*
|
||
|
|
* @param Recipe $recipe
|
||
|
|
* @param RecipeDto $dto
|
||
|
|
* @return void
|
||
|
|
*/
|
||
|
|
protected function updateRecipe(Recipe $recipe, RecipeDto $dto): void
|
||
|
|
{
|
||
|
|
$updates = [];
|
||
|
|
|
||
|
|
// Check each field for changes
|
||
|
|
if ($dto->title !== null && $dto->title !== $recipe->title) {
|
||
|
|
$updates['title'] = $dto->title;
|
||
|
|
}
|
||
|
|
if ($dto->description !== null && $dto->description !== $recipe->description) {
|
||
|
|
$updates['description'] = $dto->description;
|
||
|
|
}
|
||
|
|
if ($dto->prepTime !== null && $dto->prepTime !== $recipe->prep_time) {
|
||
|
|
$updates['prep_time'] = $dto->prepTime;
|
||
|
|
}
|
||
|
|
if ($dto->cookTime !== null && $dto->cookTime !== $recipe->cook_time) {
|
||
|
|
$updates['cook_time'] = $dto->cookTime;
|
||
|
|
}
|
||
|
|
if ($dto->totalTime !== null && $dto->totalTime !== $recipe->total_time) {
|
||
|
|
$updates['total_time'] = $dto->totalTime;
|
||
|
|
}
|
||
|
|
if ($dto->servings !== null && $dto->servings !== $recipe->servings) {
|
||
|
|
$updates['servings'] = $dto->servings;
|
||
|
|
}
|
||
|
|
if ($dto->difficulty !== null && $dto->difficulty !== $recipe->difficulty) {
|
||
|
|
$updates['difficulty'] = $dto->difficulty;
|
||
|
|
}
|
||
|
|
if ($dto->cuisine !== null && $dto->cuisine !== $recipe->cuisine) {
|
||
|
|
$updates['cuisine'] = $dto->cuisine;
|
||
|
|
}
|
||
|
|
if ($dto->category !== null && $dto->category !== $recipe->category) {
|
||
|
|
$updates['category'] = $dto->category;
|
||
|
|
}
|
||
|
|
if ($dto->calories !== null && $dto->calories !== $recipe->calories) {
|
||
|
|
$updates['calories'] = $dto->calories;
|
||
|
|
}
|
||
|
|
if ($dto->featuredImage !== null && $dto->featuredImage !== $recipe->featured_image) {
|
||
|
|
$updates['featured_image'] = $dto->featuredImage;
|
||
|
|
}
|
||
|
|
if ($dto->videoUrl !== null && $dto->videoUrl !== $recipe->video_url) {
|
||
|
|
$updates['video_url'] = $dto->videoUrl;
|
||
|
|
}
|
||
|
|
if ($dto->status !== null && $dto->status !== $recipe->status) {
|
||
|
|
$updates['status'] = $dto->status;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!empty($updates)) {
|
||
|
|
$recipe->update($updates);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Sync recipe ingredients
|
||
|
|
*
|
||
|
|
* @param Recipe $recipe
|
||
|
|
* @param RecipeDto $dto
|
||
|
|
* @return void
|
||
|
|
*/
|
||
|
|
protected function syncIngredients(Recipe $recipe, RecipeDto $dto): void
|
||
|
|
{
|
||
|
|
if (empty($dto->ingredients)) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
$syncData = [];
|
||
|
|
|
||
|
|
foreach ($dto->ingredients as $ingredientDto) {
|
||
|
|
|
||
|
|
$slug = $ingredientDto->ingredientSlug;
|
||
|
|
|
||
|
|
if (!$slug) {
|
||
|
|
$slug = Str::slug($ingredientDto->ingredientName);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Find or create ingredient
|
||
|
|
$ingredient = Ingredient::firstOrCreate(
|
||
|
|
['slug' => $slug],
|
||
|
|
[
|
||
|
|
'name' => $ingredientDto->ingredientName,
|
||
|
|
'category' => $ingredientDto->ingredientCategory,
|
||
|
|
]
|
||
|
|
);
|
||
|
|
|
||
|
|
// Prepare pivot data
|
||
|
|
$syncData[$ingredient->id] = [
|
||
|
|
'quantity' => $ingredientDto->quantity,
|
||
|
|
'unit' => $ingredientDto->unit,
|
||
|
|
'notes' => $ingredientDto->notes,
|
||
|
|
'sort_order' => $ingredientDto->sortOrder ?? 0,
|
||
|
|
];
|
||
|
|
}
|
||
|
|
|
||
|
|
$recipe->ingredients()->sync($syncData);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Sync recipe steps
|
||
|
|
*
|
||
|
|
* @param Recipe $recipe
|
||
|
|
* @param RecipeDto $dto
|
||
|
|
* @return void
|
||
|
|
*/
|
||
|
|
protected function syncSteps(Recipe $recipe, RecipeDto $dto): void
|
||
|
|
{
|
||
|
|
if (empty($dto->steps)) {
|
||
|
|
// Remove all steps if none provided
|
||
|
|
$recipe->steps()->delete();
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Delete existing steps and recreate
|
||
|
|
$recipe->steps()->delete();
|
||
|
|
|
||
|
|
foreach ($dto->steps as $stepDto) {
|
||
|
|
RecipeStep::create([
|
||
|
|
'recipe_id' => $recipe->id,
|
||
|
|
'step_number' => $stepDto->stepNumber,
|
||
|
|
'title' => $stepDto->title,
|
||
|
|
'instruction' => $stepDto->instruction,
|
||
|
|
'image' => $stepDto->image,
|
||
|
|
'time_estimate' => $stepDto->timeEstimate,
|
||
|
|
'sort_order' => $stepDto->sortOrder ?? 0,
|
||
|
|
]);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Sync recipe images
|
||
|
|
*
|
||
|
|
* @param Recipe $recipe
|
||
|
|
* @param RecipeDto $dto
|
||
|
|
* @return void
|
||
|
|
*/
|
||
|
|
protected function syncImages(Recipe $recipe, RecipeDto $dto): void
|
||
|
|
{
|
||
|
|
if (empty($dto->images)) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Delete existing images and recreate
|
||
|
|
$recipe->images()->delete();
|
||
|
|
|
||
|
|
foreach ($dto->images as $imageDto) {
|
||
|
|
RecipeImage::create([
|
||
|
|
'recipe_id' => $recipe->id,
|
||
|
|
'image_path' => $imageDto->imagePath,
|
||
|
|
'title' => $imageDto->title,
|
||
|
|
'description' => $imageDto->description,
|
||
|
|
'is_featured' => $imageDto->isFeatured ?? false,
|
||
|
|
'sort_order' => $imageDto->sortOrder ?? 0,
|
||
|
|
]);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Sync recipe nutrition
|
||
|
|
*
|
||
|
|
* @param Recipe $recipe
|
||
|
|
* @param RecipeDto $dto
|
||
|
|
* @return void
|
||
|
|
*/
|
||
|
|
protected function syncNutrition(Recipe $recipe, RecipeDto $dto): void
|
||
|
|
{
|
||
|
|
if ($dto->nutrition === null) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
RecipeNutrition::updateOrCreate(
|
||
|
|
['recipe_id' => $recipe->id],
|
||
|
|
[
|
||
|
|
'calories' => $dto->nutrition->calories,
|
||
|
|
'protein' => $dto->nutrition->protein,
|
||
|
|
'carbohydrates' => $dto->nutrition->carbohydrates,
|
||
|
|
'fat' => $dto->nutrition->fat,
|
||
|
|
'saturated_fat' => $dto->nutrition->saturatedFat,
|
||
|
|
'fiber' => $dto->nutrition->fiber,
|
||
|
|
'sugar' => $dto->nutrition->sugar,
|
||
|
|
'sodium' => $dto->nutrition->sodium,
|
||
|
|
'cholesterol' => $dto->nutrition->cholesterol,
|
||
|
|
]
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Sync recipe tags
|
||
|
|
*
|
||
|
|
* @param Recipe $recipe
|
||
|
|
* @param RecipeDto $dto
|
||
|
|
* @return void
|
||
|
|
*/
|
||
|
|
protected function syncTags(Recipe $recipe, RecipeDto $dto): void
|
||
|
|
{
|
||
|
|
if (empty($dto->tags)) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
$tagIds = [];
|
||
|
|
|
||
|
|
foreach ($dto->tags as $tagDto) {
|
||
|
|
// Find or create tag
|
||
|
|
$tag = Tag::firstOrCreate(
|
||
|
|
['slug' => $tagDto->slug],
|
||
|
|
['name' => $tagDto->name]
|
||
|
|
);
|
||
|
|
|
||
|
|
$tagIds[] = $tag->id;
|
||
|
|
}
|
||
|
|
|
||
|
|
$recipe->tags()->sync($tagIds);
|
||
|
|
}
|
||
|
|
}
|