Laravel Ecommerce Tutorial: Part 6.4, Refactoring Products

Laravel Ecommerce Tutorial: Part 6.4, Refactoring Products

Given Ncube

In the the last post we added the ability to edit products. This is part 6.4 of the on going series on building an ecommerce website in Laravel from start to deployment

As I was using the site I noticed a few things that need to be fixed

  • If a user submit the create or edit products form and not fill the optional fields we get an internal server error saying that field cannot be null, let's call this "the nulls thing"
  • On the store/update product request sku was validated as optional but it's required
  • The delete product button just deletes the product without confirmation

The nulls thing

Okay this nulls thing happens because laravel turns unfilled forms fields into nulls but in our migration we said those optional fields are not null

If we give a null the database will throw an integrity violation error.

To fix this, we filter the validated fields and remove the nulls then pass to Eloquent to do it's thing.

Let's start by editing the store method of the Admin\ProductController and add the filtering and your method should look this

/**
 * Store a newly created resource in storage.
 *
 * @param StoreProductRequest $request
 * @return RedirectResponse
 * @throws Exception
 */
public function store(StoreProductRequest $request)
{
    $product = Product::create(
        $request
            ->safe()
            ->collect()
            ->filter(fn($value) => !is_null($value))
            ->except(['images'])
            ->all(),
    );

    collect($request->validated('images'))->each(function ($image) use (
        $product,
    ) {
        $product->attachMedia(new File(storage_path('app/' . $image)));
        Storage::delete($image);
    });

    return to_route('admin.products.index')->with(
        'success',
        'Product was successfully created',
    );
}

We get the safe validated fields by calling safe then call collect to get a collection instance then we filter to make sure that values that are null are removed then remove the images key and viola!

To make sure the code is foolproof we will write a test

Open the Tests\Feature\Http\Controllers\ProductControllerTest and add the following test

use App\Models\Category;
use App\Models\Product;

public function test_can_create_product_when_optional_fields_are_not_filled()
{
    $response = $this->actingAs($this->superAdminUser())->post(
        route('admin.products.store'),
        [
            'name' => 'Test Product',
            'description' => 'Test Description',
            'price' => 100,
            'category_id' => Category::factory()->create()->id,
            'track_quantity' => 1,
            'quantity' => 10,
            'sell_out_of_stock' => 1,
            'status' => 'active',
            'sku' => Str::random(10),
            'cost' => '',
            'discounted_price' => '',
        ],
    );

    $response->assertStatus(302);
    $response->assertRedirect(route('admin.products.index'));

    $this->assertCount(1, Product::all());

    $product = Product::first();
    $this->assertEquals(0, $product->cost);
    $this->assertEquals(0, $product->discounted_price);

    $response = $this->actingAs($this->superAdminUser())->post(
        route('admin.products.store'),
        [
            'name' => 'Test Product',
            'description' => 'Test Description',
            'price' => 100,
            'category_id' => Category::factory()->create()->id,
            'track_quantity' => 0,
            'quantity' => null,
            'sell_out_of_stock' => 0,
            'cost' => 10,
            'status' => 'active',
            'sku' => Str::random(10),
        ],
    );

    $response->assertRedirect(route('admin.products.index'));
    $this->assertCount(2, Product::all());

    $product = Product::find(2);
    $this->assertEquals(10, $product->cost);
    $this->assertEquals(0, $product->discounted_price);
    $this->assertEquals(0, $product->quantity);
}

If you don't have the superAdminUser() method add it to your TestCase.php file

use App\Models\User;
use Spatie\Permission\Models\Role;
/**
 * Create a super admin user.
 *
 * @return User
 */
protected function superAdminUser(): User
{
    $user = User::factory()->create();
    $user->assignRole(Role::findOrCreate('super admin'));

    return $user;
}

In the first request we mimic what the browser would send if the user didn't fill the optional fields and if all goes well we should be redirected to the index page and there should be 1 product in the database.

Same with the next request but this time fill one optional and makes sure that it's added the product and if all goes well your tests should pass

So this was happening to the update method of the ProductController as well so let's add the filtering to that too

/**
 * Update the specified resource in storage.
 *
 * @param UpdateProductRequest $request
 * @param Product $product
 * @return RedirectResponse
 * @throws Exception
 */
public function update(UpdateProductRequest $request, Product $product)
{
    $product->update(
        $request
            ->safe()
            ->collect()
            ->filter(fn($value) => !is_null($value))
            ->except(['images'])
            ->all(),
    );

    collect($request->validated('images'))->each(function ($image) use (
        $product,
    ) {
        $product->attachMedia(new File(storage_path('app/' . $image)));
        Storage::delete($image);
    });

    return to_route('admin.products.index')->with(
        'success',
        'Product was successfully updated',
    );
}

Then write your test to make sure that this is working exactly as it should

Make sku required

This is simple all we have to do is go the products form request and change sku key from nullable or sometimes|nullable to required

Your sku validation should look like this on both StoreProductRequest and UpdateProductRequest

//StoreProductRequest
'sku' => 'required|string|unique:products,sku',


//UpdateProductReqeust
'sku' => 'required|string|unique:products,sku,' . $this->route('product')->id,

Confirm before deleting product

If a user accidentally clicks delete we want them to confirm if they actually want to delete. If a user clicks the delete button we should show a modal then make the delete

In the previous tutorials we implemented this feature, we created a stimulus controller, obliterate_controller which has an event handler handle() which fires Sweet Alert 2 asking the user to confirm and if they confirm it submits the delete form connected via stimulus target ,form

To register this controller find the div containing the delete and edit links and register the controller like this <div {{ stimulus_controller('obliterate') }}>

Find the delete link and replace it with a button and the delete form. The button has a stimulus action bound to the handle method of our controller and the form is the form target of the stimulus controller

<button {{ stimulus_action('obliterate', 'handle') }}
        class='btn btn-danger'>Delete</button>
<form {{ stimulus_target('obliterate', 'form') }}
      method="POST"
      action="{{ route('admin.products.destroy', $product) }}">
    @csrf
    @method('DELETE')
</form>

Your div containing the edit and delete links should look like this

<div {{ stimulus_controller('obliterate') }}>
    <a href='{{ route('admin.products.edit', $product) }}'
       class='btn btn-primary'>Edit</a>

    <button {{ stimulus_action('obliterate', 'handle') }}
            class='btn btn-danger'>Delete</button>
    <form {{ stimulus_target('obliterate', 'form') }}
          method="POST"
          action="{{ route('admin.products.destroy', $product) }}">
        @csrf
        @method('DELETE')
    </form>
</div>

Now if you click on delete in the products index page you should be prompted if you really want to delete and then only delete after confirmation

image showing confirmation before deleting a product

And that's it for this tutorial, in the next tutorial we add the ability to create product variations like color size etc. If you have any questions reach out to me on Twitter @ncubegiven_.

To make sure you don't miss the next post, subscribe to the newsletter below and get an email notification when it's published

And yeah always, Happy coding!