Building Ecommerce Website With PHP Lumen Laravel And Nuxtjs

Building Ecommerce Website With Lumen Laravel And Nuxtjs 7: Products CRUD

In this article of this series we will implement an important module crud in lumen which is the product CRUD, we will start by creating the controller skeleton methods and then add the code for each method

 

 

 

What we will do in this topic

  • The full product CRUD (Create, Read, Update, Delete)
  • Product images upload while creating or update
  • Adding support for creating multiple sizes of the product images
  • Updating ProductGallery model to list all the sizes of the images while retrieving products
  • Ability to delete product images.

 

Let’s get started by going to the lumen and install the popular intervention/image package:

composer require intervention/image

We will use this package to do image resizing.

Next create a new skeleton controller in app/Http/Controller named ProductsController.php

app/Http/Controller/ProductsController.php

<?php


namespace App\Http\Controllers;


use App\Models\Product;
use App\Models\ProductFeature;
use App\Models\ProductGallery;
use App\Traits\Helpers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;

class ProductsController extends Controller
{
    use Helpers;

    public function __construct()
    {
        $this->middleware('super_admin_check:store-update-destroy-destroyImage');
    }

    public function index(Request $request)
    {
        
    }

    public function store(Request $request)
    {
        
    }

    public function show($id)
    {
        
    }

    public function update(Request $request, $id)
    {
        
    }

    public function destroy($id)
    {
        
    }

    public function destroyImage($id)
    {
        
    }

    /**
     * filter and response
     *
     * @param $request
     * @param $query
     */
    private function filterAndResponse($request, $query)
    {
        
    }
}

In the constructor i called super_admin_check middleware you saw in previous tutorials to allow super admin users only to manage certain functions like store update, delete etc. Now let’s go through each CRUD action one by one:

 

Displaying All Products

To display all products this is in the index() method as shown below:

public function index(Request $request)
    {
        $query = Product::with('category', 'user', 'gallery', 'brand');

        $this->filterAndResponse($request, $query);

        $query->orderBy('id', 'DESC');

        $products = $query->paginate(15);

        return response()->json(['products' => $products], 200);
    }

    private function filterAndResponse($request, $query)
    {
        if($request->has('id')) {
            $query->where('id', $request->id);
        }

        if($request->has('title')) {
            $query->where('title', 'like', "%$request->title%");
        }

        if($request->has('product_code')) {
            $query->where('product_code', $request->product_code);
        }

        if($request->has('from_price')) {
            $query->where('price', '>=', $request->from_price);
        }

        if($request->has('to_price')) {
            $query->where('price', '<=', $request->to_price);
        }

        if($request->has('amount')) {
            $query->where('amount', $request->amount);
        }

        if($request->has('category_id')) {
            $query->where('category_id', $request->category_id);
        }

        if($request->has('brand_id')) {
            $query->where('brand_id', $request->brand_id);
        }
    }

Here we call the Product model and fetch other relations using the “with()“, this method accept the relation names and return them alongside the product result set. The filterAndResponse() method used to do some filtering on the data.

 

Creating Products

To create a new product in the store() method as shown below:

public function store(Request $request)
    {
        try {
            if(($errors = $this->doValidate($request)) && count($errors) > 0) {
                return response()->json(['success' => 0, 'message' => 'Please fix these errors', 'errors' => $errors], 500);
            }

            $product = new Product;
            foreach($request->except('features', 'image') as $key => $value) {
                if($key == 'discount_start_date' || $key == 'discount_end_date') {
                    $product->{$key} = !empty($value) && !is_null($value)?date("Y-m-d", strtotime($value)) : null;
                } else if($key == 'discount') {
                    $product->{$key} = $value?intval($value) : 0;
                } else if($key == 'brand_id') {
                    $product->{$key} = is_null($value) || empty($value) ? NULL : intval($value);
                } else {
                    $product->{$key} = $value;
                }
            }

            $product->created_by = auth()->user()->id;

            $product->save();

            // save features if any
            $this->insertFeatures($request, $product);

            // upload images
            $this->uploadImages($request, $product);

            return response()->json(['success' => 1, 'message' => 'Created successfully', 'product' => $product], 201);
        } catch (\Exception $e) {
            $product->delete();

            return response()->json(['success' => 0, 'message' => $e->getMessage()], 500);
        }
    }

At first we do dome validations on the incoming request using doValidate() method, we will implement it shortly. Then i looped through the request data excluding keys features and image using $request->except() method and bind these keys to the model then call save().

Then we need to save product features if any, for that i called insertFeatures() method which we will see below passing in the $request and the current product instance.

Finally i call uploadImages() method to upload the product images and return a success response. In the catch() block it’s beneficial to delete the product in case any error happens as shown above.

Let’s add the doValidate(), insertFeatures(), uploadImages() methods:

protected function doValidate($request, $id = null)
    {
        $payload = [
            'title' => 'required',
            'price' => 'required|numeric',
            'amount' => 'required|numeric',
            'discount' => 'min:0|max:100',
            'category_id' => 'required',
            'discount_start_date' => 'date',
            'discount_end_date' => 'date|after:discount_start_date'
        ];

        if(!$id) {
            $payload += ['image' => 'required',
                'image.*' => 'image|mimes:jpeg,png,jpg,gif,svg|max:10000'
            ];
        }

        $validator = Validator::make($request->all(), $payload);

        if($validator->fails()) {
            return $validator->errors();
        }

        return [];
    }

    rotected function insertFeatures($request, $product) {
        if($request->has('features')) {
            foreach ($request->input('features') as $id => $feature_value) {
                if(!empty($feature_value)) {
                    $productFeature = new ProductFeature();
                    $productFeature->field_id = $id;
                    $productFeature->field_value = $feature_value;

                    $product->features()->save($productFeature);
                }
            }
        }
    }

    protected function uploadImages($request, $product)
    {
        $this->createProductUploadDirs($product->id, $this->imagesSizes);

        $uploaded_files = $this->uploadFiles($request, 'image', base_path('public').'/uploads/' . $product->id);

        foreach ($uploaded_files as $uploaded_file) {

            $productGallery = new ProductGallery();
            $productGallery->image = $uploaded_file;

            $product->gallery()->save($productGallery);

            // start resize images
            foreach ($this->imagesSizes as $dirName => $imagesSize) {
                $this->resizeImage(base_path('public').'/uploads/' . $product->id . '/' . $uploaded_file, base_path('public').'/uploads/' . $product->id . '/' . $dirName . '/' . $uploaded_file, $imagesSize['width'], $imagesSize['height']);
            }
        }
    }    

In the uploadImages() method there are a lot of stuff, just be sure that you create an uploads/ directory in the public/ folder with writable permissions to store the uploaded images. This method handles uploading and resizing images so at first we call createProductUploadDirs() method we will see below to create the directories for different image sizes, then we call uploadFiles() method this method handles image upload finally we loop over the uploaded files saving them into db and doing the resize process by calling resizeImage() method, let’s add these methods in the app/Traits/Helpers.php

app/Traits/Helpers.php

<?php


namespace App\Traits;


trait Helpers
{
    protected $imagesSizes = [
        'main_slider' => ['width' => 484, 'height' => 441],
        'medium' => ['width' => 268, 'height' => 249],
        'medium2' => ['width' => 208, 'height' => 183],
        'small' => ['width' => 268, 'height' => 134],
        'product_gallery_slider' => ['width' => 84, 'height' => 84],
        'product_gallery_preview' => ['width' => 266, 'height' => 381],
        'cart_thumb' => ['width' => 110, 'height' => 110]
    ];

    function superAdminCheck()
    {
        return auth()->user()->is_super_admin == 1;
    }

    function createProductUploadDirs($product_id , $imagesSizes)
    {
        if(!file_exists(base_path('public').'/uploads/' . $product_id)) {
            @mkdir(base_path('public').'/uploads/' . $product_id, 0777);
        }

        foreach ($imagesSizes as $dirName => $imagesSize) {
            if(!file_exists(base_path('public').'/uploads/' . $product_id . '/' . $dirName)) {
                mkdir(base_path('public').'/uploads/' . $product_id . '/' . $dirName, 0777);
            }
        }
    }


    function uploadFiles($request, $filename, $destination = null)
    {
        $files_array = [];

        $destination = $destination ? $destination : base_path('public').'/uploads/';

        if($request->hasfile($filename)) {

            foreach($request->file($filename) as $image) {
                $ext = $image->getClientOriginalExtension();
                $file_name = time().md5(rand(100,999)).'.'.$ext;
                $image->move($destination, $file_name);
                @chmod($destination . '/' . $file_name, 777);
                $files_array[] = $file_name;
            }
        }

        return $files_array;
    }

    function resizeImage($imagePath, $savePath, $width, $height)
    {
        Image::make($imagePath)
            ->resize($width, $height, function ($constraint) {
                $constraint->aspectRatio();
            })->save($savePath);
    }

    public function deleteFile($path)
    {
        if(file_exists($path)) {
            @unlink($path);
        }
    }
}

That’s it the $imageSizes member variable used to store all the image sizes, you may be wondering what all these sizes, in fact we will use these different sizes when displaying products in website frontend for example we have a jquery slider that we display some lates products this slider have images width 484 and height 441 and so on.

The createProductUploadDirs() is pretty easy it checks and creates the necessary image upload directores for each product. Each product product will be stored as follows:

  • product id/main_slider
  • product id/medium
  • product id/medium2
  • and so on

For this purpose the method creates the product directory first using the product id then it loops over the $imageSizes array to create each size using the array key.

The resizeImage() method handle resizing by calling Image::make()->resize() , refer to image/intervention docs to learn more about it.

Now we need to update the ProductGallery model.

app/Models/ProductGallery.php

<?php
namespace App\Models;

use App\Traits\Helpers;
use Illuminate\Database\Eloquent\Model;

class ProductGallery extends Model
{
    use Helpers;

    protected $table = "product_gallery";

    protected $appends = ["image_url"];

    public function getImageUrlAttribute()
    {
        $urls = [];

        foreach ($this->imagesSizes as $dirName => $imagesSize) {
            $urls[$dirName] = url('/') . '/uploads/' . $this->product_id . '/' . $dirName . '/' . $this->image;
        }

        return $urls;
    }
}

As you see below we added a custom attribute image_url to list all the image sizes that we be shown when retrieving products in the json response.

 

Displaying Single Product

Implement the show() method as shown here:

public function show($id)
    {
        $product = Product::with('features', 'gallery')->findOrFail($id);

        return response()->json(['product' => $product], 200);
    }

 

Updating Products

To update product in the update() method like this:

public function update(Request $request, $id)
    {
        try {
            $product = Product::findOrFail($id);

            if(($errors = $this->doValidate($request, $id)) && count($errors) > 0) {
                return response()->json(['success' => 0, 'message' => 'Please fix these errors', 'errors' => $errors], 500);
            }

            foreach($request->except('features', 'image', '_method') as $key => $value) {
                if($key == 'discount_start_date' || $key == 'discount_end_date') {
                    $product->{$key} = !empty($value) && !is_null($value)?date("Y-m-d", strtotime($value)) : null;
                } else if($key == 'discount') {
                    $product->{$key} = $value?intval($value) : 0;
                } else if($key == 'brand_id') {
                    $product->{$key} = is_null($value) || empty($value) ? NULL : intval($value);
                } else {
                    $product->{$key} = $value;
                }
            }

            $product->save();

            if($product->features()->count() > 0) {
                $product->features()->delete();
            }

            $this->insertFeatures($request, $product);

            // upload images
            $this->uploadImages($request, $product);

            return response()->json(['success' => 1, 'message' => 'Updated successfully', 'product' => $product]);

        } catch (\Exception $e) {
            return response()->json(['success' => 0, 'message' => $e->getMessage()], 500);
        }
    }

Here we follow the same approach as we did in the store() method except that we must retrieve the current product to update, then we do the same validations and save. Notice that before we save the features we should delete them first to delete the old features for this product.

 

 

Removing Products

Removing products is done in the destroy() method as shown below:

public function destroy($id)
    {
        try {
            $product = Product::with('gallery')->findOrFail($id);

            foreach ($product->gallery as $gallery) {
                if(!empty($gallery->image)) {
                    foreach ($gallery->image_url as $dir => $url) {
                        $this->deleteFile(base_path('public').'/uploads/' . $gallery->product_id . '/' . $dir . '/' . $gallery->image);
                    }

                    $this->deleteFile(base_path('public').'/uploads/' . $gallery->product_id . '/' . $gallery->image);
                }
            }

            $product->delete();

            return response()->json(['success' => 1, 'message' => 'Deleted successfully'], 200);
        } catch (\Exception $e) {
            return response()->json(['success' => 0, 'message' => $e->getMessage()], 500);
        }
    }

The destroy() method is all about deleting the product and all it’s images so in the code above we retrieve the product along with the product gallery, then we loop over all the gallery delete all image sizes by using the image_url key that we added in ProductGallery model above and the method that handles delete is deleteFile() method, this method in the Helpers trait.

 

Removing Product Image

It’s important that we have the ability to remove product images from product gallery, so for this purpose i created a custom action the destroyImage():

public function destroyImage($id)
    {
        try {
            $gallery = ProductGallery::findOrFail($id);

            if(!empty($gallery->image)) {
                foreach ($gallery->image_url as $dir => $url) {
                    $this->deleteFile(base_path('public').'/uploads/' . $gallery->product_id . '/' . $dir . '/' . $gallery->image);
                }

                $this->deleteFile(base_path('public').'/uploads/' . $gallery->product_id . '/' . $gallery->image);
            }

            $gallery->delete();

            return response()->json(['success' => 1, 'message' => 'Deleted successfully'], 200);
        } catch (\Exception $e) {
            return response()->json(['success' => 0, 'message' => $e->getMessage()], 500);
        }
    }

The code above we must insure the delete a single image file from the disk it’s similar to the destory method.

 

The Full Product Controller

app/Http/Controllers/ProductsController.php

<?php


namespace App\Http\Controllers;


use App\Models\Product;
use App\Models\ProductFeature;
use App\Models\ProductGallery;
use App\Traits\Helpers;
use App\Traits\HomeApi;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;

class ProductsController extends Controller
{
    use Helpers, HomeApi;

    public function __construct()
    {
        $this->middleware('super_admin_check:store-update-destroy-destroyImage');
    }

    public function index(Request $request)
    {
        $query = Product::with('category', 'user', 'gallery', 'brand');

        $this->filterAndResponse($request, $query);

        $query->orderBy('id', 'DESC');

        $products = $query->paginate(15);

        return response()->json(['products' => $products], 200);
    }

    public function store(Request $request)
    {
        try {
            if(($errors = $this->doValidate($request)) && count($errors) > 0) {
                return response()->json(['success' => 0, 'message' => 'Please fix these errors', 'errors' => $errors], 500);
            }

            $product = new Product;
            foreach($request->except('features', 'image') as $key => $value) {
                if($key == 'discount_start_date' || $key == 'discount_end_date') {
                    $product->{$key} = !empty($value) && !is_null($value)?date("Y-m-d", strtotime($value)) : null;
                } else if($key == 'discount') {
                    $product->{$key} = $value?intval($value) : 0;
                } else if($key == 'brand_id') {
                    $product->{$key} = is_null($value) || empty($value) ? NULL : intval($value);
                } else {
                    $product->{$key} = $value;
                }
            }

            $product->created_by = auth()->user()->id;

            $product->save();

            // save features if any
            $this->insertFeatures($request, $product);

            // upload images
            $this->uploadImages($request, $product);

            return response()->json(['success' => 1, 'message' => 'Created successfully', 'product' => $product], 201);
        } catch (\Exception $e) {
            $product->delete();

            return response()->json(['success' => 0, 'message' => $e->getMessage()], 500);
        }
    }

    public function show($id)
    {
        $product = Product::with('features', 'gallery')->findOrFail($id);

        return response()->json(['product' => $product], 200);
    }

    public function update(Request $request, $id)
    {
        try {
            $product = Product::findOrFail($id);

            if(($errors = $this->doValidate($request, $id)) && count($errors) > 0) {
                return response()->json(['success' => 0, 'message' => 'Please fix these errors', 'errors' => $errors], 500);
            }

            foreach($request->except('features', 'image', '_method') as $key => $value) {
                if($key == 'discount_start_date' || $key == 'discount_end_date') {
                    $product->{$key} = !empty($value) && !is_null($value)?date("Y-m-d", strtotime($value)) : null;
                } else if($key == 'discount') {
                    $product->{$key} = $value?intval($value) : 0;
                } else if($key == 'brand_id') {
                    $product->{$key} = is_null($value) || empty($value) ? NULL : intval($value);
                } else {
                    $product->{$key} = $value;
                }
            }

            $product->save();

            if($product->features()->count() > 0) {
                $product->features()->delete();
            }

            $this->insertFeatures($request, $product);

            // upload images
            $this->uploadImages($request, $product);

            return response()->json(['success' => 1, 'message' => 'Updated successfully', 'product' => $product]);

        } catch (\Exception $e) {
            return response()->json(['success' => 0, 'message' => $e->getMessage()], 500);
        }
    }

    public function destroy($id)
    {
        try {
            $product = Product::with('gallery')->findOrFail($id);

            foreach ($product->gallery as $gallery) {
                if(!empty($gallery->image)) {
                    foreach ($gallery->image_url as $dir => $url) {
                        $this->deleteFile(base_path('public').'/uploads/' . $gallery->product_id . '/' . $dir . '/' . $gallery->image);
                    }

                    $this->deleteFile(base_path('public').'/uploads/' . $gallery->product_id . '/' . $gallery->image);
                }
            }

            $product->delete();

            return response()->json(['success' => 1, 'message' => 'Deleted successfully'], 200);
        } catch (\Exception $e) {
            return response()->json(['success' => 0, 'message' => $e->getMessage()], 500);
        }
    }

    public function destroyImage($id)
    {
        try {
            $gallery = ProductGallery::findOrFail($id);

            if(!empty($gallery->image)) {
                foreach ($gallery->image_url as $dir => $url) {
                    $this->deleteFile(base_path('public').'/uploads/' . $gallery->product_id . '/' . $dir . '/' . $gallery->image);
                }

                $this->deleteFile(base_path('public').'/uploads/' . $gallery->product_id . '/' . $gallery->image);
            }

            $gallery->delete();

            return response()->json(['success' => 1, 'message' => 'Deleted successfully'], 200);
        } catch (\Exception $e) {
            return response()->json(['success' => 0, 'message' => $e->getMessage()], 500);
        }
    }

    /**
     * filter and response
     *
     * @param $request
     * @param $query
     */
    private function filterAndResponse($request, $query)
    {
        if($request->has('id')) {
            $query->where('id', $request->id);
        }

        if($request->has('title')) {
            $query->where('title', 'like', "%$request->title%");
        }

        if($request->has('product_code')) {
            $query->where('product_code', $request->product_code);
        }

        if($request->has('from_price')) {
            $query->where('price', '>=', $request->from_price);
        }

        if($request->has('to_price')) {
            $query->where('price', '<=', $request->to_price);
        }

        if($request->has('amount')) {
            $query->where('amount', $request->amount);
        }

        if($request->has('category_id')) {
            $query->where('category_id', $request->category_id);
        }

        if($request->has('brand_id')) {
            $query->where('brand_id', $request->brand_id);
        }
    }

    protected function insertFeatures($request, $product) {
        if($request->has('features')) {
            foreach ($request->input('features') as $id => $feature_value) {
                if(!empty($feature_value)) {
                    $productFeature = new ProductFeature();
                    $productFeature->field_id = $id;
                    $productFeature->field_value = $feature_value;

                    $product->features()->save($productFeature);
                }
            }
        }
    }


    /**
     * upload images
     *
     * @param $request
     * @param $product
     */
    protected function uploadImages($request, $product)
    {
        $this->createProductUploadDirs($product->id, $this->imagesSizes);

        $uploaded_files = $this->uploadFiles($request, 'image', base_path('public').'/uploads/' . $product->id);

        foreach ($uploaded_files as $uploaded_file) {

            $productGallery = new ProductGallery();
            $productGallery->image = $uploaded_file;

            $product->gallery()->save($productGallery);

            // start resize images
            foreach ($this->imagesSizes as $dirName => $imagesSize) {
                $this->resizeImage(base_path('public').'/uploads/' . $product->id . '/' . $uploaded_file, base_path('public').'/uploads/' . $product->id . '/' . $dirName . '/' . $uploaded_file, $imagesSize['width'], $imagesSize['height']);
            }
        }
    }

    /**
     * validate
     *
     * @param $request
     * @param null $id
     * @throws \Exception
     */
    protected function doValidate($request, $id = null)
    {
        $payload = [
            'title' => 'required',
            'price' => 'required|numeric',
            'amount' => 'required|numeric',
            'discount' => 'min:0|max:100',
            'category_id' => 'required',
            'discount_start_date' => 'date',
            'discount_end_date' => 'date|after:discount_start_date'
        ];

        if(!$id) {
            $payload += ['image' => 'required',
                'image.*' => 'image|mimes:jpeg,png,jpg,gif,svg|max:10000'
            ];
        }

        $validator = Validator::make($request->all(), $payload);

        if($validator->fails()) {
            return $validator->errors();
        }

        return [];
    }
}

 

Updating Routes

Let’s update web/routes.php

<?php

$router->get('/', function () use ($router) {
    return $router->app->version();
});

$router->group(['prefix' => 'api'], function () use ($router) {
    $router->post('/login', 'Auth\\LoginController@login');
    $router->post('/register', 'Auth\\RegisterController@register');

    $router->group(['prefix' => 'category'], function () use ($router) {
        $router->get('/', 'CategoriesController@index');
        $router->get('/htmltree', 'CategoriesController@getCategoryHtmlTree');
        $router->get('/{id}', 'CategoriesController@show');
    });

    $router->group(['prefix' => 'brand'], function () use ($router) {
        $router->get('/', 'BrandsController@index');
        $router->get('/{id}', 'BrandsController@show');
    });

    $router->group(['prefix' => 'product'], function () use ($router) {
        $router->get('/', 'ProductsController@index');
        $router->get('/{id}', 'ProductsController@show');
    });

    $router->group(['middleware' => 'auth:api'], function () use ($router) {
        $router->get('/me', 'Auth\\LoginController@userDetails');
        $router->get('/logout', 'Auth\\LoginController@logout');
        $router->get('/check-login', 'Auth\\LoginController@checkLogin');

        $router->group(['prefix' => 'category'], function () use ($router) {
            $router->post('/', 'CategoriesController@store');
            $router->put('/{id}', 'CategoriesController@update');
            $router->delete('/{id}', 'CategoriesController@destroy');
        });

        $router->group(['prefix' => 'brand'], function () use ($router) {
            $router->post('/', 'BrandsController@store');
            $router->put('/{id}', 'BrandsController@update');
            $router->delete('/{id}', 'BrandsController@destroy');
        });

        $router->group(['prefix' => 'product'], function () use ($router) {
           $router->post('/', 'ProductsController@store');
            $router->put('/{id}', 'ProductsController@update');
            $router->delete('/delete-image/{id}', 'ProductsController@destroyImage');
            $router->delete('/{id}', 'ProductsController@destroy');
        });
    });
});

In the next part we will add these pieces togther when working on NuxtJs project when displaying and handling products

 

Continue to Part8: Manage Products In Admin Panel

 

0 0 vote
Article Rating
Share this: