Laravel Refactoring
Refactoring is a very important rule to look back on when working on a project, the importance of refactoring code means future legacy code will be more readable and therefore appear less complicated to add or resolve possible issues (Read more).
Example
This example builds on the refactoring lessons learnt from Laracon 2019. In the example below there are 84 lines of code that can be moved elsewhere. In doing so commenting may be made redundant as function names should speak for themselves and therefore the use of a comment is not necessary.
/**
* Store a newly created resource in storage.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function store(Request $request)
{
// Current User
$user = Auth::user();
// Validation Rules
$rules = array(
'title' => 'required|string',
'description' => 'required|string',
'recipe_utensils' => 'nullable',
'prep_time' => 'required|string',
'cook_time' => 'required|string',
// Ingredients
'amount' => 'required|array',
'measurement' => 'required|array',
'ingredient' => 'required|array',
// Steps
'recipe_steps' => 'nullable',
'servings' => 'nullable',
'difficulty' => 'nullable',
'public' => 'nullable',
);
// Validation Messages
$messages = array(
// Recipe Title validation messages
'title.required' => 'Give your recipe a title',
// Recipe Description validation messages
'description.required' => 'Give your recipe a description',
'prep_time.required' => 'Recipe prep time is required',
'cook_time.required' => 'Recipe cooking time is required',
// Recipe Ingredients validation messages
'amount.required' => 'Please include ingredients.',
'measurement.required' => 'Please include ingredients.',
'ingredient.required' => 'Please include ingredients.',
);
// Validate Request against rules
$validator = Validator::make($request->all(), $rules, $messages);
// If Validation fails
if ($validator->fails()) {
return redirect()->back()->withErrors($validator)->withInput();
}
// Create new Recipe
$recipe = Recipe::create([
'user_id' => Auth::user()->id,
'title' => $request->input('recipe_title'),
'description' => $request->input('recipe_description'),
'prep_time' => $request->input('recipe_prep_time'),
'cook_time' => $request->input('recipe_cook_time'),
'servings' => $request->input('recipe_servings'),
'difficulty' => $request->input('recipe_difficulty'),
'slug' => AppHelper::createSlug($request->input('recipe_title')),
]);
// Create Recipe Steps
foreach ($request->input('recipe_step') as $key => $description) {
RecipeStep::create([
'recipe_id' $recipe->id,
'step' => $key + 1,
'description' => $description,
])
}
// Create Recipe Ingredients
foreach ($request->input('recipe_ingredients') as $key => $ingredient) {
$ingredient = explode('|', $ingredient);
RecipeIngredient::create([
'recipe_id' $recipe->id,
'option_id' => $ingredient[0],
'amount' => $ingredient[1],
'unit' => $ingredient[2],
])
}
return redirect('/my-recipes')->with('message', 'Successfully created Recipe!');
}
First Refactor
Complex validation scenarios such as this one where there are 46 lines of code handling the Validation alone calls for a Form Request. Using a Form Request means that we can completely remove all these lines of code and let a single class handle the functionality.
php artisan make:request AddRecipeRequest
/**
* Store a newly created resource in storage.
*
* @param \Illuminate\Http\AddRecipeRequest $request
* @return \Illuminate\Http\Response
*/
public function store(AddRecipeRequest $request)
{
// Current User
$user = Auth::user();
// Create new Recipe
$recipe = Recipe::create([
'user_id' => Auth::user()->id,
'title' => $request->input('recipe_title'),
'description' => $request->input('recipe_description'),
'prep_time' => $request->input('recipe_prep_time'),
'cook_time' => $request->input('recipe_cook_time'),
'servings' => $request->input('recipe_servings'),
'difficulty' => $request->input('recipe_difficulty'),
'slug' => AppHelper::createSlug($request->input('recipe_title')),
]);
// Create Recipe Steps
foreach ($request->input('recipe_step') as $key => $description) {
RecipeStep::create([
'recipe_id' $recipe->id,
'step' => $key + 1,
'description' => $description,
])
}
// Create Recipe Ingredients
foreach ($request->input('recipe_ingredients') as $key => $ingredient) {
$ingredient = explode('|', $ingredient);
RecipeIngredient::create([
'recipe_id' $recipe->id,
'option_id' => $ingredient[0],
'amount' => $ingredient[1],
'unit' => $ingredient[2],
])
}
return redirect('/my-recipes')->with('message', 'Successfully created Recipe!');
}
Second Refactor
Request parameters keys and database column names should ideally match, this way we can send the entire Validation request into the create method itself.
Furthermore, we can create a Model Observer to handle other additional parameters,
such as storing the user_id
or creating a slug.
php artisan make:observer RecipeObserver --model=Recipe
public function store(AddRecipeRequest $request)
{
$recipe = Recipe::create($request->validated());
// Create Recipe Steps
foreach ($request->input('recipe_step') as $key => $description) {
RecipeStep::create([
'recipe_id' $recipe->id,
'step' => $key + 1,
'description' => $description,
])
}
// Create Recipe Ingredients
foreach ($request->input('recipe_ingredients') as $key => $ingredient) {
$ingredient = explode('|', $ingredient);
RecipeIngredient::create([
'recipe_id' $recipe->id,
'option_id' => $ingredient[0],
'amount' => $ingredient[1],
'unit' => $ingredient[2],
])
}
return redirect('/my-recipes')->with('message', 'Successfully created Recipe!');
}
// App\Observers\RecipeObserver
class RecipeObserver
{
public function creating(Recipe $recipe)
{
$recipe->public = $recipe->public ? 1 : 0;
$recipe->user_id = auth()->id();
$recipe->slug = str_slug($recipe->title);
}
}
// App\Providers\AppServiceProvider
class AppServiceProvider extends ServiceProvider
{
public function boot()
{
Recipe::observe(RecipeObserver::class);
}
}
Third Refactor
Laravel has many variations on the create()
CRUD method. However in this
scenario to reduce code and to make code more readable we can create new separate
methods for relationship events. Furthermore, in doing so Laravel
automatically adds the foreign key for us when calling the
model's relationship function.
/**
* Store a newly created resource in storage.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function store(AddRecipeRequest $request)
{
$recipe = Recipe::create($request->validated());
// Create Recipe Steps
foreach ($request->input('recipe_step') as $key => $description) {
$recipe->addIngredient([
'step' => $key + 1,
'description' => $description,
]);
}
// Create Recipe Ingredients
foreach ($request->input('recipe_ingredients') as $key => $ingredient) {
$ingredient = explode('|', $ingredient);
$recipe->addStep([
'option_id' => $ingredient[0],
'amount' => $ingredient[1],
'unit' => $ingredient[2],
]);
}
return redirect('/my-recipes')->with('message', 'Successfully created Recipe!');
}
class Recipe extends Model
{
public function addIngredient($ingredient)
{
$this->ingredients()->create($ingredient);
}
public function addStep($step)
{
$this->steps()->create($step);
}
}
Fourth Refactor
In conclusion Laravel Controllers should be as simple and small as possible. They are there to perform actions, therefore on the event of adding multiple records can be their own methods too.
In doing so we now we have 7 lines of code.
/**
* Store a newly created resource in storage.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function store(AddRecipeRequest $request)
{
$recipe = Recipe::create($request->validated());
$recipe->addSteps($request->input('recipe_steps'));
$recipe->addIngredients($request->only(['amount', 'measurement', 'ingredient']));
return redirect('/my-recipes')->with('success', 'Successfully created Recipe!');
}