with('tags', Recipe::getTagTotals()); } /** * Show the form for creating a new resource. * * @return \Illuminate\Contracts\View\View */ public function create(): View { return $this->edit(new Recipe()); } /** * Store a newly created resource in storage. * * @param \Illuminate\Http\Request $request * @return \Illuminate\Http\RedirectResponse * * @throws \Throwable */ public function store(Request $request): RedirectResponse { return $this->update($request, new Recipe()); } /** * Display the specified resource. * * @param \App\Models\Recipe $recipe * @return \Illuminate\Contracts\View\View */ public function show(Recipe $recipe): View { // Set feature image if media has been added. $feature_image = NULL; if ($recipe->hasMedia() && $recipe->getFirstMedia()->hasGeneratedConversion('header')) { $feature_image = $recipe->getFirstMediaUrl('default', 'header'); } return view('recipes.show') ->with('recipe', $recipe) ->with('feature_image', $feature_image); } /** * Show the form for editing the specified resource. * * @param \App\Models\Recipe $recipe * @return \Illuminate\Contracts\View\View */ public function edit(Recipe $recipe): View { // Pre-populate relationships from form data or current recipe. $ingredients = []; if ($old = old('ingredients')) { foreach ($old['id'] as $key => $ingredient_id) { if (empty($ingredient_id)) { continue; } $ingredients[] = [ 'original_key' => $old['original_key'][$key], 'amount' => $old['amount'][$key], 'unit' => $old['unit'][$key], 'ingredient_id' => $ingredient_id, 'ingredient_type' => $old['type'][$key], 'ingredient_name' => $old['name'][$key], 'detail' => $old['detail'][$key], ]; } } else { foreach ($recipe->ingredientAmounts as $key => $ingredientAmount) { $ingredients[] = [ 'original_key' => $key, 'amount' => $ingredientAmount->amount_formatted, 'unit' => $ingredientAmount->unit, 'ingredient_id' => $ingredientAmount->ingredient_id, 'ingredient_type' => $ingredientAmount->ingredient_type, 'ingredient_name' => $ingredientAmount->ingredient->name, 'detail' => $ingredientAmount->detail, ]; } } $steps = []; if ($old = old('steps')) { foreach ($old['step'] as $key => $step) { if (empty($step)) { continue; } $steps[] = [ 'original_key' => $old['original_key'][$key], 'step_default' => $step, ]; } } else { foreach ($recipe->steps as $key => $step) { $steps[] = [ 'original_key' => $key, 'step_default' => $step->step, ]; } } // Convert string tags (from old form data) to a Collection. $recipe_tags = old('tags', $recipe->tags->pluck('name')); if (is_string($recipe_tags)) { $recipe_tags = new Collection(explode(',', $recipe_tags)); } return view('recipes.edit') ->with('recipe', $recipe) ->with('recipe_tags', $recipe_tags) ->with('ingredients', $ingredients) ->with('steps', $steps) ->with('ingredients_units', Nutrients::$units); } /** * Update the specified resource in storage. * * @param \Illuminate\Http\Request $request * @param \App\Models\Recipe $recipe * * @return \Illuminate\Http\RedirectResponse * * @throws \Throwable */ public function update(Request $request, Recipe $recipe): RedirectResponse { $input = $request->validate([ 'name' => ['required', 'string'], 'description' => ['nullable', 'string'], 'description_delta' => ['nullable', 'string'], 'image' => ['nullable', 'file', 'mimes:jpg,png,gif'], 'remove_image' => ['nullable', 'boolean'], 'servings' => ['required', 'numeric'], 'time_prep' => ['nullable', 'numeric'], 'time_cook' => ['nullable', 'numeric'], 'weight' => ['nullable', 'numeric'], 'source' => ['nullable', 'string'], 'ingredients.amount' => ['required', 'array', new ArrayNotEmpty], 'ingredients.amount.*' => ['required_with:ingredients.id.*', 'nullable', new StringIsDecimalOrFraction], 'ingredients.unit' => ['required', 'array'], 'ingredients.unit.*' => ['required_with:ingredients.id.*'], 'ingredients.detail' => ['required', 'array'], 'ingredients.detail.*' => 'nullable|string', 'ingredients.id' => ['required', 'array', new ArrayNotEmpty], 'ingredients.id.*' => 'required_with:ingredients.amount.*|nullable', 'ingredients.type' => ['required', 'array', new ArrayNotEmpty], 'ingredients.type.*' => ['required_with:ingredients.id.*', 'nullable', new UsesIngredientTrait()], 'ingredients.original_key' => 'nullable|array', 'steps.step' => ['required', 'array', new ArrayNotEmpty], 'steps.step.*' => 'nullable|string', 'steps.original_key' => 'nullable|array', ]); // Validate that no ingredients are recursive. // TODO: refactor as custom validator. foreach (array_filter($input['ingredients']['id']) as $key => $id) { if ($input['ingredients']['type'][$key] == Recipe::class && $id == $recipe->id) { return back()->withInput()->withErrors('To understand recursion, you must understand recursion. Remove this recipe from this recipe.'); } } $recipe->fill([ 'name' => Str::lower($input['name']), 'description' => $input['description'], 'description_delta' => $input['description_delta'], 'servings' => (int) $input['servings'], 'weight' => $input['weight'], 'time_prep' => (int) $input['time_prep'], 'time_cook' => (int) $input['time_cook'], 'source' => $input['source'], ]); try { DB::transaction(function () use ($recipe, $input) { $recipe->saveOrFail(); // Delete any removed ingredients. $removed = array_diff($recipe->ingredientAmounts->keys()->all(), $input['ingredients']['original_key']); foreach ($removed as $removed_key) { $recipe->ingredientAmounts[$removed_key]->delete(); } // Add/update current ingredients. $ingredient_amounts = []; $weight = 0; foreach (array_filter($input['ingredients']['id']) as $key => $ingredient_id) { if (!is_null($input['ingredients']['original_key'][$key])) { $ingredient_amounts[$key] = $recipe->ingredientAmounts[$input['ingredients']['original_key'][$key]]; } else { $ingredient_amounts[$key] = new IngredientAmount(); } $ingredient_amounts[$key]->fill([ 'amount' => Number::floatFromString($input['ingredients']['amount'][$key]), 'unit' => $input['ingredients']['unit'][$key], 'detail' => $input['ingredients']['detail'][$key], 'weight' => $weight++, ]); $ingredient_amounts[$key]->ingredient() ->associate($input['ingredients']['type'][$key]::where('id', $ingredient_id)->first()); } $recipe->ingredientAmounts()->saveMany($ingredient_amounts); $steps = []; $number = 1; // Delete any removed steps. $removed = array_diff($recipe->steps->keys()->all(), $input['steps']['original_key']); foreach ($removed as $removed_key) { $recipe->steps[$removed_key]->delete(); } foreach (array_filter($input['steps']['step']) as $key => $step) { if (!is_null($input['steps']['original_key'][$key])) { $steps[$key] = $recipe->steps[$input['steps']['original_key'][$key]]; } else { $steps[$key] = new RecipeStep(); } $steps[$key]->fill(['number' => $number++, 'step' => $step]); } $recipe->steps()->saveMany($steps); }); } catch (\Exception $e) { DB::rollBack(); return back()->withInput()->withErrors($e->getMessage()); } // Sync tags. if ($tags = $request->get('tags')) { $recipe->syncTags(explode(',', $tags)); } // Handle recipe image. if (!empty($input['image'])) { /** @var \Illuminate\Http\UploadedFile $file */ $file = $input['image']; $recipe->clearMediaCollection(); $recipe ->addMediaFromRequest('image') ->usingName($recipe->name) ->usingFileName("{$recipe->slug}.{$file->extension()}") ->toMediaCollection(); } elseif (isset($input['remove_image']) && (bool) $input['remove_image']) { $recipe->clearMediaCollection(); } session()->flash('message', "Recipe {$recipe->name} updated!"); return redirect()->route('recipes.show', $recipe); } /** * Confirm removal of specified resource. */ public function delete(Recipe $recipe): View { return view('recipes.delete')->with('recipe', $recipe); } /** * Remove the specified resource from storage. */ public function destroy(Recipe $recipe): RedirectResponse { // Remove recipe ingredients. foreach ($recipe->ingredientAmounts as $ia) { $ia->delete(); } // Remove the recipe from any recipes. foreach ($recipe->ingredientAmountRelationships as $iar) { $iar->delete(); } // Remove the recipe. $recipe->delete(); return redirect(route('recipes.index')) ->with('message', "Recipe {$recipe->name} deleted!"); } }