mirror of https://github.com/kcal-app/kcal.git
393 lines
14 KiB
PHP
393 lines
14 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers;
|
|
|
|
use App\Http\Requests\UpdateRecipeRequest;
|
|
use App\Models\Food;
|
|
use App\Models\IngredientAmount;
|
|
use App\Models\Recipe;
|
|
use App\Models\RecipeSeparator;
|
|
use App\Models\RecipeStep;
|
|
use App\Support\Number;
|
|
use App\Support\Nutrients;
|
|
use Illuminate\Contracts\View\View;
|
|
use Illuminate\Http\RedirectResponse;
|
|
use Illuminate\Support\Collection;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Str;
|
|
|
|
class RecipeController extends Controller
|
|
{
|
|
/**
|
|
* Display a listing of the resource.
|
|
*
|
|
* @return \Illuminate\Contracts\View\View
|
|
*/
|
|
public function index(): View
|
|
{
|
|
return view('recipes.index')->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.
|
|
*
|
|
* @throws \Throwable
|
|
*/
|
|
public function store(UpdateRecipeRequest $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) {
|
|
$ingredients[$key] = [
|
|
'type' => 'ingredient',
|
|
'key' => $old['key'][$key],
|
|
'weight' => $old['weight'][$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],
|
|
];
|
|
|
|
// Add supported units for the ingredient.
|
|
$ingredient = NULL;
|
|
if ($ingredients[$key]['ingredient_type'] === Food::class) {
|
|
$ingredient = Food::whereId($ingredients[$key]['ingredient_id'])->first();
|
|
}
|
|
elseif ($ingredients[$key]['ingredient_type'] === Recipe::class) {
|
|
$ingredient = Recipe::whereId($ingredients[$key]['ingredient_id'])->first();
|
|
}
|
|
if ($ingredient) {
|
|
$ingredients[$key]['units_supported'] = $ingredient->units_supported;
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
foreach ($recipe->ingredientAmounts as $key => $ingredientAmount) {
|
|
$ingredients[] = [
|
|
'type' => 'ingredient',
|
|
'key' => $key,
|
|
'weight' => $ingredientAmount->weight,
|
|
'amount' => $ingredientAmount->amount_formatted,
|
|
'unit' => $ingredientAmount->unit,
|
|
'units_supported' => $ingredientAmount->ingredient->units_supported,
|
|
'ingredient_id' => $ingredientAmount->ingredient_id,
|
|
'ingredient_type' => $ingredientAmount->ingredient_type,
|
|
'ingredient_name' => $ingredientAmount->ingredient->name,
|
|
'detail' => $ingredientAmount->detail,
|
|
];
|
|
}
|
|
}
|
|
|
|
$separators = [];
|
|
if ($old = old('separators')) {
|
|
foreach ($old['key'] as $index => $key) {
|
|
$separators[] = [
|
|
'type' => 'separator',
|
|
'key' => $old['key'][$index],
|
|
'weight' => $old['weight'][$index],
|
|
'text' => $old['text'][$index],
|
|
];
|
|
}
|
|
}
|
|
else {
|
|
foreach ($recipe->ingredientSeparators as $key => $ingredientSeparator) {
|
|
$separators[] = [
|
|
'type' => 'separator',
|
|
'key' => $key,
|
|
'weight' => $ingredientSeparator->weight,
|
|
'text' => $ingredientSeparator->text,
|
|
];
|
|
}
|
|
}
|
|
|
|
$steps = [];
|
|
if ($old = old('steps')) {
|
|
foreach ($old['step'] as $key => $step) {
|
|
if (empty($step)) {
|
|
continue;
|
|
}
|
|
$steps[] = [
|
|
'key' => $old['key'][$key],
|
|
'step_default' => $step,
|
|
];
|
|
}
|
|
}
|
|
else {
|
|
foreach ($recipe->steps as $key => $step) {
|
|
$steps[] = [
|
|
'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_list', new Collection([...$ingredients, ...$separators]))
|
|
->with('steps', $steps)
|
|
->with('units_supported', Nutrients::units()->toArray());
|
|
}
|
|
|
|
/**
|
|
* Update the specified resource in storage.
|
|
*
|
|
* @throws \Throwable
|
|
*/
|
|
public function update(UpdateRecipeRequest $request, Recipe $recipe): RedirectResponse
|
|
{
|
|
$input = $request->validated();
|
|
|
|
// 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'],
|
|
'volume' => $input['volume'],
|
|
'time_prep' => (int) $input['time_prep'],
|
|
'time_cook' => (int) $input['time_cook'],
|
|
'source' => $input['source'],
|
|
]);
|
|
if (!empty($input['volume'])) {
|
|
$recipe->volume = Number::floatFromString($input['volume']);
|
|
}
|
|
|
|
try {
|
|
DB::transaction(function () use ($input, $recipe, $request) {
|
|
$recipe->saveOrFail();
|
|
$this->updateIngredients($recipe, $input);
|
|
$this->updateIngredientSeparators($recipe, $input);
|
|
$this->updateSteps($recipe, $input);
|
|
|
|
$tags = $request->get('tags', []);
|
|
if (!empty($tags)) {
|
|
$recipe->syncTags(explode(',', $tags));
|
|
}
|
|
elseif ($recipe->tags->isNotEmpty()) {
|
|
$recipe->detachTags($recipe->tags);
|
|
}
|
|
|
|
// Refresh and index updated tags.
|
|
$recipe->fresh()->searchable();
|
|
});
|
|
} catch (\Exception $e) {
|
|
DB::rollBack();
|
|
return back()->withInput()->withErrors($e->getMessage());
|
|
}
|
|
|
|
// 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']) && $input['remove_image']) {
|
|
$recipe->clearMediaCollection();
|
|
}
|
|
|
|
session()->flash('message', "Recipe {$recipe->name} updated!");
|
|
return redirect()->route('recipes.show', $recipe);
|
|
}
|
|
|
|
/**
|
|
* Updates recipe ingredients data based on input.
|
|
*
|
|
* @param \App\Models\Recipe $recipe
|
|
* @param array $input
|
|
*
|
|
* @throws \Exception
|
|
*/
|
|
private function updateIngredients(Recipe $recipe, array $input): void {
|
|
// Delete any removed ingredients.
|
|
$removed = array_diff($recipe->ingredientAmounts->keys()->all(), $input['ingredients']['key']);
|
|
foreach ($removed as $removed_key) {
|
|
$recipe->ingredientAmounts[$removed_key]->delete();
|
|
}
|
|
|
|
// Add/update current ingredients.
|
|
$ingredient_amounts = [];
|
|
foreach (array_filter($input['ingredients']['id']) as $key => $ingredient_id) {
|
|
if (!is_null($input['ingredients']['key'][$key])) {
|
|
$ingredient_amounts[$key] = $recipe->ingredientAmounts[$input['ingredients']['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' => (int) $input['ingredients']['weight'][$key],
|
|
]);
|
|
$ingredient_amounts[$key]->ingredient()
|
|
->associate($input['ingredients']['type'][$key]::where('id', $ingredient_id)->first());
|
|
}
|
|
$recipe->ingredientAmounts()->saveMany($ingredient_amounts);
|
|
}
|
|
|
|
/**
|
|
* Updates recipe steps data based on input.
|
|
*
|
|
* @param \App\Models\Recipe $recipe
|
|
* @param array $input
|
|
*
|
|
* @throws \Exception
|
|
*/
|
|
private function updateSteps(Recipe $recipe, array $input): void {
|
|
$steps = [];
|
|
$number = 1;
|
|
|
|
// Delete any removed steps.
|
|
$removed = array_diff($recipe->steps->keys()->all(), $input['steps']['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']['key'][$key])) {
|
|
$steps[$key] = $recipe->steps[$input['steps']['key'][$key]];
|
|
}
|
|
else {
|
|
$steps[$key] = new RecipeStep();
|
|
}
|
|
$steps[$key]->fill(['number' => $number++, 'step' => $step]);
|
|
}
|
|
$recipe->steps()->saveMany($steps);
|
|
}
|
|
|
|
/**
|
|
* Updates recipe ingredient separators data based on input.
|
|
*
|
|
* @param \App\Models\Recipe $recipe
|
|
* @param array $input
|
|
*
|
|
* @throws \Exception
|
|
*/
|
|
private function updateIngredientSeparators(Recipe $recipe, array $input): void {
|
|
// Take no action of remove all separators
|
|
if (!isset($input['separators']) || empty($input['separators'])) {
|
|
if ($recipe->ingredientSeparators->isNotEmpty()) {
|
|
$recipe->ingredientSeparators()->delete();
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Delete any removed separators.
|
|
$removed = array_diff($recipe->ingredientSeparators->keys()->all(), $input['separators']['key']);
|
|
foreach ($removed as $removed_key) {
|
|
$recipe->ingredientSeparators[$removed_key]->delete();
|
|
}
|
|
|
|
// Add/update current separators.
|
|
$ingredient_separators = [];
|
|
foreach ($input['separators']['key'] as $index => $key) {
|
|
if (!is_null($key)) {
|
|
$ingredient_separators[$index] = $recipe->ingredientSeparators[$key];
|
|
}
|
|
else {
|
|
$ingredient_separators[$index] = new RecipeSeparator();
|
|
}
|
|
$ingredient_separators[$index]->fill([
|
|
'container' => 'ingredients',
|
|
'text' => $input['separators']['text'][$index],
|
|
'weight' => (int) $input['separators']['weight'][$index],
|
|
]);
|
|
|
|
}
|
|
$recipe->ingredientSeparators()->saveMany($ingredient_separators);
|
|
}
|
|
|
|
/**
|
|
* 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!");
|
|
}
|
|
|
|
}
|