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).

  1. Example
  2. First Refactor
  3. Second Refactor
  4. Third Refactor
  5. Fourth Refactor
  6. References

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

Form and database column names should ideally match, this way we can simply send the entire Validation request into the create method itself. On doing so when the keys match the model columns, the value will auto-set. Furthermore, we can create an observer to handle the various model events, such as storing the user_id or creating a slug.

php artisan make:observer RecipeObserver --model=Recipe

/**
* 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) {
      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!');
}


class RecipeObserver
{
    /**
     * Handle the recipe "creating" event.
     *
     * @param  \App\Recipe  $recipe
     * @return void
     */
    public function creating(Recipe $recipe)
    {
        $recipe->public = $recipe->public ? 1 : 0;
        $recipe->user_id = auth()->id();
        $recipe->slug = str_slug($recipe->title);
    }
}

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!');
}

References