DEV Community

Cover image for Laravel 8 - Service Class (Writing clean code in 3 Steps) ๐Ÿงน๐Ÿ“”๐Ÿง‘๐Ÿปโ€๐Ÿ’ป
DaleLanto
DaleLanto

Posted on

Laravel 8 - Service Class (Writing clean code in 3 Steps) ๐Ÿงน๐Ÿ“”๐Ÿง‘๐Ÿปโ€๐Ÿ’ป

Scenario of what Service Classes is used for:

Suppose that we have a controller that looks something like this:

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class CartItemController extends Controller
{
    /**
     * Display a listing of the resource.
     *
     * @return \Illuminate\Http\Response
     */
    public function index()
    {
        $content = session()->has('cart') ? session()->get('cart') : collect([]);
        $total = $content->reduce(function ($total, $item) {
            return $total += $item->get('price') * $item->get('quantity');
        });

        return view('cart.index', [
            'content' => $content,
            'total' => $total,
        ]);
    }

    /**
     * Store a newly created resource in storage.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    public function store(Request $request)
    {
        $request->validate([
            'name' => 'required|string',
            'price' => 'required|numeric',
            'quantity' => 'required|integer',
        ]);

        $cartItem = collect([
            'name' => $request->name,
            'price' => floatval($request->price),
            'quantity' => intval($request->quantity),
            'options' => $request->options,
        ]);

        $content = session()->has('cart') ? session()->get('cart') : collect([]);

        $id = request('id');

        if ($content->has($id)) {
            $cartItem->put('quantity', $content->get($id)->get('quantity') + $request->quantity);
        }

        $content->put($id, $cartItem);

        session()->put('content', $content);

        return back()->with('success', 'Item added to cart');
    }

    /**
     * Display the specified resource.
     *
     * @param  int  $id
     * @return \Illuminate\Http\Response
     */
    public function show($id)
    {
        $content = session()->get('cart');

        if ($content->has($id)) {
            $item = $content->get($id);

            return view('cart', compact('item'));
        }

        return back()->with('fail', 'Item not found');
    }

    /**
     * Update the specified resource in storage.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  int  $id
     * @return \Illuminate\Http\Response
     */
    public function update(Request $request, $id)
    {
        $content = session()->get('cart');

        if ($content->has($id)) {
            $cartItem = $content->get($id);

            switch ($request->action) {
                case 'plus':
                    $cartItem->put('quantity', $content->get($id)->get('quantity') + 1);
                    break;
                case 'minus':
                    $updatedQuantity = $content->get($id)->get('quantity') - 1;

                    if ($updatedQuantity < 1) {
                        $updatedQuantity = 1;
                    }

                    $cartItem->put('quantity', $updatedQuantity);
                    break;
            }

            $content->put($id, $cartItem);

            session()->put('cart', $content);

            return back()->with('success', 'Item updated in cart');
        }

        return back()->with('fail', 'Item not found');
    }

    /**
     * Remove the specified resource from storage.
     *
     * @param  int  $id
     * @return \Illuminate\Http\Response
     */
    public function destroy($id)
    {
        $content = session()->get('cart');

        if ($content->has($id)) {
            session()->put('cart', $content->except($id));

            return back()->with('success', 'Item removed from cart');
        }

        return back()->with('fail', 'Item not found');
    }
}
Enter fullscreen mode Exit fullscreen mode

This is a controller from an imaginary e-commerce project, responsible for managing the shopping cart.

Although this is a perfectly valid piece of code, there are some problems.

The controller does too much.

A controller should be only responsible for transporting requests and responses. The inner details aka the business logic should be handled by other classes.

What we want is something that looks like this:

Image description

Business logic or domain logic is that part of the program which encodes the real-world business rules that determine how data can be created, stored, and changed.

Image description

Now let's start simplifying the code and transferring the business logic to the service class!

First create a file for the service class

Step 1: Create the folder Services and the file CartService.php

Image description

Step 2: Transfer the first function index in Services/CartService.php

<?php

namespace App\Services;

use Illuminate\Support\Collection;
use Illuminate\Session\SessionManager;

class CartService {
    const MINIMUM_QUANTITY = 1;
    const DEFAULT_INSTANCE = 'shopping-cart';

    protected $session;
    protected $instance;

    /**
     * Constructs a new cart object.
     *
     * @param Illuminate\Session\SessionManager $session
     */
    public function __construct(SessionManager $session)
    {
        $this->session = $session;
    }

    /**
     * Adds a new item to the cart.
     *
     * @param string $id
     * @param string $name
     * @param string $price
     * @param string $quantity
     * @param array $options
     * @return void
     */

/**
     * Returns the content of the cart.
     *
     * @return Illuminate\Support\Collection
     */
    public function content(): Collection
    {
        return is_null($this->session->get(self::DEFAULT_INSTANCE)) ? collect([]) : $this->session->get(self::DEFAULT_INSTANCE);
    }

    /**
     * Returns total price of the items in the cart.
     *
     * @return string
     */
    public function total(): string
    {
        $content = $this->getContent();

        $total = $content->reduce(function ($total, $item) {
            return $total += $item->get('price') * $item->get('quantity');
        });

        return number_format($total, 2);
    }

}
Enter fullscreen mode Exit fullscreen mode

Step 3: Use the code in Services/CartService.php into the CartController.php

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Services\CartService;
use App\Http\Requests\CartItemRequest;

class CartItemController extends Controller
{
    protected $cartService;

    /**
     * Instantiate a new controller instance.
     *
     * @return void
     */
    public function __construct(CartService $cartService)
    {
        $this->cartService = $cartService;
    }

    /**
     * Display a listing of the resource.
     *
     * @return \Illuminate\Http\Response
     */
    public function index()
    {
        $content = $this->cartService->content();
        $total = $this->cartService->total();

        return view('cart.index', [
            'content' => $content,
            'total' => $total,
        ]);
    }
}
Enter fullscreen mode Exit fullscreen mode

The functions content() and total() are responsible for returning the cart content and total price of added items respectively.

Hurray! We have successfully simplified one function of our controller, you can easily do the same thing on the other functions to make your code better, cleaner, readable and scalable!

Image description

If you want to read mode about using services, here are some good real-world examples:

  1. Laravel Examples

  2. So Documentation

  3. Laravel Daily

Now that you're still here you may want the complete result of the conversion from the example above and you're lucky because the whole answer is just below!

in Services/CartService.php update the code to this:

<?php

namespace App\Services;

use Illuminate\Support\Collection;
use Illuminate\Session\SessionManager;

class CartService {
    const MINIMUM_QUANTITY = 1;
    const DEFAULT_INSTANCE = 'shopping-cart';

    protected $session;
    protected $instance;

    /**
     * Constructs a new cart object.
     *
     * @param Illuminate\Session\SessionManager $session
     */
    public function __construct(SessionManager $session)
    {
        $this->session = $session;
    }

    /**
     * Adds a new item to the cart.
     *
     * @param string $id
     * @param string $name
     * @param string $price
     * @param string $quantity
     * @param array $options
     * @return void
     */
    public function add($id, $name, $price, $quantity, $options = []): void
    {
        $cartItem = $this->createCartItem($name, $price, $quantity, $options);

        $content = $this->getContent();

        if ($content->has($id)) {
            $cartItem->put('quantity', $content->get($id)->get('quantity') + $quantity);
        }

        $content->put($id, $cartItem);

        $this->session->put(self::DEFAULT_INSTANCE, $content);
    }

    /**
     * Updates the quantity of a cart item.
     *
     * @param string $id
     * @param string $action
     * @return void
     */
    public function update(string $id, string $action): void
    {
        $content = $this->getContent();

        if ($content->has($id)) {
            $cartItem = $content->get($id);

            switch ($action) {
                case 'plus':
                    $cartItem->put('quantity', $content->get($id)->get('quantity') + 1);
                    break;
                case 'minus':
                    $updatedQuantity = $content->get($id)->get('quantity') - 1;

                    if ($updatedQuantity < self::MINIMUM_QUANTITY) {
                        $updatedQuantity = self::MINIMUM_QUANTITY;
                    }

                    $cartItem->put('quantity', $updatedQuantity);
                    break;
            }

            $content->put($id, $cartItem);

            $this->session->put(self::DEFAULT_INSTANCE, $content);
        }
    }

    /**
     * Removes an item from the cart.
     *
     * @param string $id
     * @return void
     */
    public function remove(string $id): void
    {
        $content = $this->getContent();

        if ($content->has($id)) {
            $this->session->put(self::DEFAULT_INSTANCE, $content->except($id));
        }
    }

    /**
     * Clears the cart.
     *
     * @return void
     */
    public function clear(): void
    {
        $this->session->forget(self::DEFAULT_INSTANCE);
    }

    /**
     * Returns the content of the cart.
     *
     * @return Illuminate\Support\Collection
     */
    public function content(): Collection
    {
        return is_null($this->session->get(self::DEFAULT_INSTANCE)) ? collect([]) : $this->session->get(self::DEFAULT_INSTANCE);
    }

    /**
     * Returns total price of the items in the cart.
     *
     * @return string
     */
    public function total(): string
    {
        $content = $this->getContent();

        $total = $content->reduce(function ($total, $item) {
            return $total += $item->get('price') * $item->get('quantity');
        });

        return number_format($total, 2);
    }

    /**
     * Returns the content of the cart.
     *
     * @return Illuminate\Support\Collection
     */
    protected function getContent(): Collection
    {
        return $this->session->has(self::DEFAULT_INSTANCE) ? $this->session->get(self::DEFAULT_INSTANCE) : collect([]);
    }

    /**
     * Creates a new cart item from given inputs.
     *
     * @param string $name
     * @param string $price
     * @param string $quantity
     * @param array $options
     * @return Illuminate\Support\Collection
     */
    protected function createCartItem(string $name, string $price, string $quantity, array $options): Collection
    {
        $price = floatval($price);
        $quantity = intval($quantity);

        if ($quantity < self::MINIMUM_QUANTITY) {
            $quantity = self::MINIMUM_QUANTITY;
        }

        return collect([
            'name' => $name,
            'price' => $price,
            'quantity' => $quantity,
            'options' => $options,
        ]);
    }
}
Enter fullscreen mode Exit fullscreen mode

Now the best part is here!

Image description

in Controllers/CartController.php update the code to this:

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Services\CartService;
use App\Http\Requests\CartItemRequest;

class CartItemController extends Controller
{
    protected $cartService;

    /**
     * Instantiate a new controller instance.
     *
     * @return void
     */
    public function __construct(CartService $cartService)
    {
        $this->cartService = $cartService;
    }

    /**
     * Display a listing of the resource.
     *
     * @return \Illuminate\Http\Response
     */
    public function index()
    {
        $content = $this->cartService->content();
        $total = $this->cartService->total();

        return view('cart.index', [
            'content' => $content,
            'total' => $total,
        ]);
    }

    /**
     * Store a newly created resource in storage.
     *
     * @param  \App\Http\Requests\CartItemRequest  $request
     * @return \Illuminate\Http\Response
     */
    public function store(CartItemRequest $request)
    {
        $this->cartService->add($request->id, $request->name, $request->price, $request->quantity, $request->options);

        return back()->with('success', 'Item added to cart');
    }

    /**
     * Display the specified resource.
     *
     * @param  int  $id
     * @return \Illuminate\Http\Response
     */
    public function show($id)
    {
        $content = $this->cartService->content();

        $item = $content->get($id);

        return view('cart', compact('item'));
    }

    /**
     * Update the specified resource in storage.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  int  $id
     * @return \Illuminate\Http\Response
     */
    public function update(Request $request, $id)
    {
        $this->cartService->update($id, $request->id);

        return back()->with('success', 'Item updated in cart');
    }

    /**
     * Remove the specified resource from storage.
     *
     * @param  int  $id
     * @return \Illuminate\Http\Response
     */
    public function destroy($id)
    {
        $this->cartService->remove($id);

        return back()->with('success', 'Item removed from cart');
    }
}
Enter fullscreen mode Exit fullscreen mode

Now ain't that controller clean? Also you can use the services we created in your other controllers as you see fit!

Image description

Discussion (1)

Collapse
programm011 profile image
Nizomiddin Zaripov

๐Ÿ‘๐Ÿ‘