• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

kcal-app / kcal / 8953420469

04 May 2024 09:37PM UTC coverage: 92.994% (-0.4%) from 93.391%
8953420469

push

github

cdubz
Correct PHPMA config

1407 of 1513 relevant lines covered (92.99%)

14.05 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

79.12
/app/Http/Controllers/RecipeController.php
1
<?php
2

3
namespace App\Http\Controllers;
4

5
use App\Http\Requests\UpdateRecipeRequest;
6
use App\Models\Food;
7
use App\Models\IngredientAmount;
8
use App\Models\Recipe;
9
use App\Models\RecipeSeparator;
10
use App\Models\RecipeStep;
11
use App\Support\Number;
12
use App\Support\Nutrients;
13
use Illuminate\Contracts\View\View;
14
use Illuminate\Http\RedirectResponse;
15
use Illuminate\Http\Request;
16
use Illuminate\Support\Collection;
17
use Illuminate\Support\Facades\DB;
18
use Illuminate\Support\Str;
19

20
class RecipeController extends Controller
21
{
22
    /**
23
     * Display a listing of the resource.
24
     *
25
     * @return \Illuminate\Contracts\View\View
26
     */
27
    public function index(): View
28
    {
29
        return view('recipes.index')->with('tags', Recipe::getTagTotals());
2✔
30
    }
31

32
    /**
33
     * Show the form for creating a new resource.
34
     *
35
     * @return \Illuminate\Contracts\View\View
36
     */
37
    public function create(): View
38
    {
39
        return $this->edit(new Recipe());
1✔
40
    }
41

42
    /**
43
     * Store a newly created resource in storage.
44
     *
45
     * @throws \Throwable
46
     */
47
    public function store(UpdateRecipeRequest $request): RedirectResponse
48
    {
49
        return $this->update($request, new Recipe());
1✔
50
    }
51

52
    /**
53
     * Display the specified resource.
54
     *
55
     * @param  \App\Models\Recipe  $recipe
56
     * @return \Illuminate\Contracts\View\View
57
     */
58
    public function show(Recipe $recipe): View
59
    {
60
        // Set feature image if media has been added.
61
        $feature_image = NULL;
2✔
62
        if ($recipe->hasMedia() && $recipe->getFirstMedia()->hasGeneratedConversion('header')) {
2✔
63
            $feature_image = $recipe->getFirstMediaUrl('default', 'header');
1✔
64
        }
65

66
        return view('recipes.show')
2✔
67
            ->with('recipe', $recipe)
2✔
68
            ->with('feature_image', $feature_image);
2✔
69
    }
70

71
    /**
72
     * Show the form for editing the specified resource.
73
     *
74
     * @param \App\Models\Recipe $recipe
75
     * @return \Illuminate\Contracts\View\View
76
     */
77
    public function edit(Recipe $recipe): View
78
    {
79
        // Pre-populate relationships from form data or current recipe.
80
        $ingredients = [];
3✔
81
        if ($old = old('ingredients')) {
3✔
82
            foreach ($old['id'] as $key => $ingredient_id) {
1✔
83
                $ingredients[$key] = [
1✔
84
                    'type' => 'ingredient',
1✔
85
                    'key' => $old['key'][$key],
1✔
86
                    'weight' => $old['weight'][$key],
1✔
87
                    'amount' => $old['amount'][$key],
1✔
88
                    'unit' => $old['unit'][$key],
1✔
89
                    'ingredient_id' => $ingredient_id,
1✔
90
                    'ingredient_type' => $old['type'][$key],
1✔
91
                    'ingredient_name' => $old['name'][$key],
1✔
92
                    'detail' => $old['detail'][$key],
1✔
93
                ];
1✔
94

95
                // Add supported units for the ingredient.
96
                $ingredient = NULL;
×
97
                if ($ingredients[$key]['ingredient_type'] === Food::class) {
×
98
                    $ingredient = Food::whereId($ingredients[$key]['ingredient_id'])->first();
×
99
                }
100
                elseif ($ingredients[$key]['ingredient_type'] === Recipe::class) {
×
101
                    $ingredient = Recipe::whereId($ingredients[$key]['ingredient_id'])->first();
×
102
                }
103
                if ($ingredient) {
×
104
                    $ingredients[$key]['units_supported'] = $ingredient->units_supported;
×
105
                }
106
            }
107
        }
108
        else {
109
            foreach ($recipe->ingredientAmounts as $key => $ingredientAmount) {
2✔
110
                $ingredients[] = [
1✔
111
                    'type' => 'ingredient',
1✔
112
                    'key' => $key,
1✔
113
                    'weight' => $ingredientAmount->weight,
1✔
114
                    'amount' => $ingredientAmount->amount_formatted,
1✔
115
                    'unit' => $ingredientAmount->unit,
1✔
116
                    'units_supported' => $ingredientAmount->ingredient->units_supported,
1✔
117
                    'ingredient_id' => $ingredientAmount->ingredient_id,
1✔
118
                    'ingredient_type' => $ingredientAmount->ingredient_type,
1✔
119
                    'ingredient_name' => $ingredientAmount->ingredient->name,
1✔
120
                    'detail' => $ingredientAmount->detail,
1✔
121
                ];
1✔
122
            }
123
        }
124

125
        $separators = [];
2✔
126
        if ($old = old('separators')) {
2✔
127
            foreach ($old['key'] as $index => $key) {
×
128
                $separators[] = [
×
129
                    'type' => 'separator',
×
130
                    'key' => $old['key'][$index],
×
131
                    'weight' => $old['weight'][$index],
×
132
                    'text' => $old['text'][$index],
×
133
                ];
×
134
            }
135
        }
136
        else {
137
            foreach ($recipe->ingredientSeparators as $key => $ingredientSeparator) {
2✔
138
                $separators[] = [
1✔
139
                    'type' => 'separator',
1✔
140
                    'key' => $key,
1✔
141
                    'weight' => $ingredientSeparator->weight,
1✔
142
                    'text' => $ingredientSeparator->text,
1✔
143
                ];
1✔
144
            }
145
        }
146

147
        $steps = [];
2✔
148
        if ($old = old('steps')) {
2✔
149
            foreach ($old['step'] as $key => $step) {
×
150
                if (empty($step)) {
×
151
                    continue;
×
152
                }
153
                $steps[] = [
×
154
                    'key' => $old['key'][$key],
×
155
                    'step_default' => $step,
×
156
                ];
×
157
            }
158
        }
159
        else {
160
            foreach ($recipe->steps as $key => $step) {
2✔
161
                $steps[] = [
1✔
162
                    'key' => $key,
1✔
163
                    'step_default' => $step->step,
1✔
164
                ];
1✔
165
            }
166
        }
167

168
        // Convert string tags (from old form data) to a Collection.
169
        $recipe_tags = old('tags', $recipe->tags->pluck('name'));
2✔
170
        if (is_string($recipe_tags)) {
2✔
171
            $recipe_tags = new Collection(explode(',', $recipe_tags));
×
172
        }
173

174
        return view('recipes.edit')
2✔
175
            ->with('recipe', $recipe)
2✔
176
            ->with('recipe_tags', $recipe_tags)
2✔
177
            ->with('ingredients_list', new Collection([...$ingredients, ...$separators]))
2✔
178
            ->with('steps', $steps)
2✔
179
            ->with('units_supported', Nutrients::units()->toArray());
2✔
180
    }
181

182
    /**
183
     * Update the specified resource in storage.
184
     *
185
     * @throws \Throwable
186
     */
187
    public function update(UpdateRecipeRequest $request, Recipe $recipe): RedirectResponse
188
    {
189
        $input = $request->validated();
2✔
190

191
        // Validate that no ingredients are recursive.
192
        // TODO: refactor as custom validator.
193
        foreach (array_filter($input['ingredients']['id']) as $key => $id) {
2✔
194
            if ($input['ingredients']['type'][$key] == Recipe::class && $id == $recipe->id) {
2✔
195
                return back()->withInput()->withErrors('To understand recursion, you must understand recursion. Remove this recipe from this recipe.');
×
196
            }
197
        }
198

199
        $recipe->fill([
2✔
200
            'name' => Str::lower($input['name']),
2✔
201
            'description' => $input['description'],
2✔
202
            'description_delta' => $input['description_delta'],
2✔
203
            'servings' => (int) $input['servings'],
2✔
204
            'weight' => $input['weight'],
2✔
205
            'volume' => $input['volume'],
2✔
206
            'time_prep' => (int) $input['time_prep'],
2✔
207
            'time_cook' => (int) $input['time_cook'],
2✔
208
            'source' => $input['source'],
2✔
209
        ]);
2✔
210
        if (!empty($input['volume'])) {
2✔
211
            $recipe->volume = Number::floatFromString($input['volume']);
×
212
        }
213

214
        try {
215
            DB::transaction(function () use ($input, $recipe, $request) {
2✔
216
                $recipe->saveOrFail();
2✔
217
                $this->updateIngredients($recipe, $input);
2✔
218
                $this->updateIngredientSeparators($recipe, $input);
2✔
219
                $this->updateSteps($recipe, $input);
2✔
220
                $recipe->updateTagsFromRequest($request);
2✔
221
            });
2✔
222
        } catch (\Exception $e) {
×
223
            DB::rollBack();
×
224
            return back()->withInput()->withErrors($e->getMessage());
×
225
        }
226

227
        // Handle recipe image.
228
        if (!empty($input['image'])) {
2✔
229
            /** @var \Illuminate\Http\UploadedFile $file */
230
            $file = $input['image'];
2✔
231
            $recipe->clearMediaCollection();
2✔
232
            $recipe
2✔
233
                ->addMediaFromRequest('image')
2✔
234
                ->usingName($recipe->name)
2✔
235
                ->usingFileName("{$recipe->slug}.{$file->extension()}")
2✔
236
                ->toMediaCollection();
2✔
237
        }
238
        elseif (isset($input['remove_image']) && $input['remove_image']) {
×
239
            $recipe->clearMediaCollection();
×
240
        }
241

242
        session()->flash('message', "Recipe {$recipe->name} updated!");
2✔
243
        return redirect()->route('recipes.show', $recipe);
2✔
244
    }
245

246
    /**
247
     * Updates recipe ingredients data based on input.
248
     *
249
     * @param \App\Models\Recipe $recipe
250
     * @param array $input
251
     *
252
     * @throws \Exception
253
     */
254
    private function updateIngredients(Recipe $recipe, array $input): void {
255
        // Delete any removed ingredients.
256
        $removed = array_diff($recipe->ingredientAmounts->keys()->all(), $input['ingredients']['key']);
2✔
257
        foreach ($removed as $removed_key) {
2✔
258
            $recipe->ingredientAmounts[$removed_key]->delete();
×
259
        }
260

261
        // Add/update current ingredients.
262
        $ingredient_amounts = [];
2✔
263
        foreach (array_filter($input['ingredients']['id']) as $key => $ingredient_id) {
2✔
264
            if (!is_null($input['ingredients']['key'][$key])) {
2✔
265
                $ingredient_amounts[$key] = $recipe->ingredientAmounts[$input['ingredients']['key'][$key]];
1✔
266
            }
267
            else {
268
                $ingredient_amounts[$key] = new IngredientAmount();
1✔
269
            }
270
            $ingredient_amounts[$key]->fill([
2✔
271
                'amount' => Number::floatFromString($input['ingredients']['amount'][$key]),
2✔
272
                'unit' => $input['ingredients']['unit'][$key],
2✔
273
                'detail' => $input['ingredients']['detail'][$key],
2✔
274
                'weight' => (int) $input['ingredients']['weight'][$key],
2✔
275
            ]);
2✔
276
            $ingredient_amounts[$key]->ingredient()
2✔
277
                ->associate($input['ingredients']['type'][$key]::where('id', $ingredient_id)->first());
2✔
278
        }
279
        $recipe->ingredientAmounts()->saveMany($ingredient_amounts);
2✔
280
    }
281

282
    /**
283
     * Updates recipe steps data based on input.
284
     *
285
     * @param \App\Models\Recipe $recipe
286
     * @param array $input
287
     *
288
     * @throws \Exception
289
     */
290
    private function updateSteps(Recipe $recipe, array $input): void {
291
        $steps = [];
2✔
292
        $number = 1;
2✔
293

294
        // Delete any removed steps.
295
        $removed = array_diff($recipe->steps->keys()->all(), $input['steps']['key']);
2✔
296
        foreach ($removed as $removed_key) {
2✔
297
            $recipe->steps[$removed_key]->delete();
×
298
        }
299

300
        foreach (array_filter($input['steps']['step']) as $key => $step) {
2✔
301
            if (!is_null($input['steps']['key'][$key])) {
2✔
302
                $steps[$key] = $recipe->steps[$input['steps']['key'][$key]];
1✔
303
            }
304
            else {
305
                $steps[$key] = new RecipeStep();
1✔
306
            }
307
            $steps[$key]->fill(['number' => $number++, 'step' => $step]);
2✔
308
        }
309
        $recipe->steps()->saveMany($steps);
2✔
310
    }
311

312
    /**
313
     * Updates recipe ingredient separators data based on input.
314
     *
315
     * @param \App\Models\Recipe $recipe
316
     * @param array $input
317
     *
318
     * @throws \Exception
319
     */
320
    private function updateIngredientSeparators(Recipe $recipe, array $input): void {
321
        // Take no action of remove all separators
322
        if (!isset($input['separators']) || empty($input['separators'])) {
2✔
323
            if ($recipe->ingredientSeparators->isNotEmpty()) {
×
324
                $recipe->ingredientSeparators()->delete();
×
325
            }
326
            return;
×
327
        }
328

329
        // Delete any removed separators.
330
        $removed = array_diff($recipe->ingredientSeparators->keys()->all(), $input['separators']['key']);
2✔
331
        foreach ($removed as $removed_key) {
2✔
332
            $recipe->ingredientSeparators[$removed_key]->delete();
×
333
        }
334

335
        // Add/update current separators.
336
        $ingredient_separators = [];
2✔
337
        foreach ($input['separators']['key'] as $index => $key) {
2✔
338
            if (!is_null($key)) {
2✔
339
                $ingredient_separators[$index] = $recipe->ingredientSeparators[$key];
1✔
340
            }
341
            else {
342
                $ingredient_separators[$index] = new RecipeSeparator();
1✔
343
            }
344
            $ingredient_separators[$index]->fill([
2✔
345
                'container' => 'ingredients',
2✔
346
                'text' => $input['separators']['text'][$index],
2✔
347
                'weight' => (int) $input['separators']['weight'][$index],
2✔
348
            ]);
2✔
349

350
        }
351
        $recipe->ingredientSeparators()->saveMany($ingredient_separators);
2✔
352
    }
353

354
    /**
355
     * Confirm duplicating recipe.
356
     */
357
    public function duplicateConfirm(Recipe $recipe): View {
358
        return view('recipes.duplicate')->with('recipe', $recipe);
1✔
359
    }
360

361
    /**
362
     * Duplicate a recipe.
363
     */
364
    public function duplicate(Request $request, Recipe $recipe): RedirectResponse
365
    {
366
        $attributes = $request->validate(['name' => ['required', 'string']]);
1✔
367

368
        try {
369
            $new_recipe = $recipe->duplicate($attributes);
1✔
370
        } catch (\Throwable $e) {
×
371
            return back()->withInput()->withErrors($e->getMessage());
×
372
        }
373

374
        return redirect()->route('recipes.show', $new_recipe)
1✔
375
            ->with('message', "Recipe {$recipe->name} duplicated!");
1✔
376
    }
377

378
    /**
379
     * Confirm removal of specified resource.
380
     */
381
    public function delete(Recipe $recipe): View
382
    {
383
        return view('recipes.delete')->with('recipe', $recipe);
1✔
384
    }
385

386
    /**
387
     * Remove the specified resource from storage.
388
     */
389
    public function destroy(Recipe $recipe): RedirectResponse
390
    {
391
        // Remove recipe ingredients.
392
        foreach ($recipe->ingredientAmounts as $ia) {
1✔
393
            $ia->delete();
1✔
394
        }
395

396
        // Remove the recipe from any recipes.
397
        foreach ($recipe->ingredientAmountRelationships as $iar) {
1✔
398
            $iar->delete();
×
399
        }
400

401
        // Remove the recipe.
402
        $recipe->delete();
1✔
403
        return redirect(route('recipes.index'))
1✔
404
            ->with('message', "Recipe {$recipe->name} deleted!");
1✔
405
    }
406

407
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc