Backend for pizza shop on Laravel 8

This article is the second in a series of articles about building a pizza shop with Laravel 8 + Inertia. Read the first article about creating migrations and models for a project.

Today I will talk about what you need to create a backend for a shop based on Laravel 8, or rather about creating Routes, Controllers, Services and Resources.

Creating routes in Laravel 8

Laravel 8 routes are still created in a file located at routes/web.php relative to the project root. They are responsible for ensuring that different paths entered by the user in the browser address bar are processed by different controllers. For a pizza shop, we need 6 routes.

Route::get('/', [PizzaController::class, 'index'])->name('index');
Route::get('/cart', [PizzaController::class, 'cart'])->name('cart');
Route::get('/checkout', [PizzaController::class, 'checkout'])->name('checkout');
Route::get('/pizza/{pizza_id}', [PizzaController::class, 'pizzaPage'])->where('pizza_id', '[0-9_\-]+')->name('pizzaPage');
Route::get('/success', [PizzaController::class, 'success'])->name('success');
Route::middleware(['auth:sanctum', 'verified'])->get('/dashboard',  [OrderController::class, 'dashboard'])->name('dashboard');

The structure of routes has changed slightly in the new version of Laravel. Now route controllers are not specified in quotes, but they need to be connected at the top of the file code as follows.

use App\Http\Controllers\PizzaController;
use App\Http\Controllers\OrderController;

So the route syntax is now.

Route::get('/', [PizzaController::class, 'index'])->name('index');

Here: Route::get – HTTP verb (may be post, put …); ‘/’ – path relative to your site that users will drive in to access the route; PizzaController::class – call to the class of the controller that handles the request; ‘index’ – method in the controller that will process the request;  name('index') – the name by which it will be possible to refer to the route within the application.

Of the 6 Laravel routes described above:

  1. Responsible for displaying the main page;
  2. Displaying the cart page;
  3. Checkout page;
  4. Displaying a single product page;
  5. Page of successful order creation;
  6. Protected route (for personal account) for displaying the user’s order.

Hooray, we have created the usual routes for Laravel 8. Now we need routes for the API, that is, those that will process requests coming to the backend from the frontend (without reloading the page).

Api routes in Laravel 8

To create Api routes in Laravel 8, you need to make changes to the routes/api.php file. To create a small store that we are doing, we only need 5 api routes.

Route::get('/get_pizzas', [PizzaController::class, 'getPizzas'])->name('get_pizzas');
Route::get('/get_pizza/{pizza_id}', [PizzaController::class, 'getPizza'])->where('pizza_id', '[0-9_\-]+')->name('get_pizza');
Route::get('/get_cart', [PizzaController::class, 'getCart'])->name('get_cart');
Route::post('/checkout', [PizzaController::class, 'postCheckout'])->name('post_checkout');

Route::middleware('auth:sanctum')->get('/get_user_orders', [OrderController::class, 'getUserOrders'])->name('get_user_orders');

Their structure is exactly the same as for ordinary routes, it is worth noting only 2 things that I did not mention earlier. This is the where('pizza_id', '[0-9_\-]+') block – it means that in the pizza_id variable specified in the path, only characters corresponding to the following regular expression (that is, in this case, numbers) can be used. And the middleware('auth:sanctum') block means that this route is protected by the Laravel Sanctum authentication system (I will talk about it later), in short, only authorized users will be able to access this route.

Of the 5 Laravel routes described above:

  1. Receives all available products (in our store – pizzas) for display on the home page;
  2. Gets a single product to show on the product page;
  3. Receives the user’s cart, that is, the products that the user added to the cart and their value;
  4. Post-request creating a user’s order;
  5. Receiving orders of the user for display in his personal account.

Api controllers used in Api routes also need to be connected at the top of the page:

use App\Http\Controllers\Api\PizzaController;
use App\Http\Controllers\Api\OrderController;

This completes the creation of Api routes, it’s time to move on to creating controllers.

Creating controllers in Laravel 8

The controllers accessed by the main routes are located in Laravel 8 under the app\Http\Controllers folder relative to the root. I created 2 controllers, PizzaController and OrderController, both extending the standard controller that was already in this folder.

PizzaController is simply designed to display frontend files created in Inertia.js (I will write in detail about its creation in the next article). Therefore, at the top of the controller, I connect the appropriate library.

use Inertia\Inertia;

And the methods in the controller implement those that were previously set in the routes and simply include the corresponding Inertia files (more about this in the next article, subscribe to my Twitter so as not to miss).

public function index()
{
    return Inertia::render('Index');
}
public function pizzaPage()
{
    return Inertia::render('Pizza');
}
public function cart(Request $request)
{
    return Inertia::render('Cart');
}
public function checkout(Request $request)
{
    return Inertia::render('Checkout');
}
public function success(Request $request)
{
    return Inertia::render('Success');
}

OrderController contains only 1 dashboard function, in which we receive the current Request and UserService using dependency injection (we will discuss creating a service later in this article). In the method from request, we get the token of the current user, and if there is exist, we pass it to the frontend.

public function dashboard(Request $request, UserService $userService)
{
    $user_token = $userService->getUserToken($request);
    if (!is_null($user_token))
        Inertia::share('user_token', $user_token->plainTextToken);
    return Inertia::render('Orders');
}

Do not forget to connect the classes used at the top of the controller.

use Illuminate\Http\Request;
use Inertia\Inertia;

Now let’s move to the Api controllers that are accessed by the Api routes. I will place them in a separate folder app\Http\Controllers\Api and there will also be two of them: PizzaController and OrderController.

The first controller will have 3 methods that will simply redirect the request to the appropriate service, which will do all the work for them. We will create the service a little later.

public function getPizzas(PizzaService $pizzaService, Request $request)
{
    return $pizzaService->getPizzas($request);
}

public function getPizza(PizzaService $pizzaService, Request $request)
{
    return $pizzaService->getPizza($request);
}

public function getCart(PizzaService $pizzaService, Request $request)
{
    return $pizzaService->getCart($request);
}

Another method will be create a new order. Internally, it validates the incoming request, gets the user to which the order will be linked, then receives the products that the user has selected in the shop and creates a new order. Everywhere, in case of success, the method passes the “success” flag to the frontend.

public function postCheckout(PizzaService $pizzaService, UserService $userService, OrderService $orderService, Request $request)
{
    $validator = $pizzaService->validateCheckout($request->all());
    if ($validator->fails()) {
        return [
            'success' => false,
            'errors' => $validator->errors()
        ];
    }
    try {
        $user = $userService->getUserForCheckout($request);
        $pizzas = $pizzaService->getPizzasForCheckout($request);
        if (!$pizzas)
            return [ 'success' => false ];

        $orderService->newOrder()->calculateSubtotal($request, $pizzas)->fillOrder($request, $user)->generateOrderItems($request, $pizzas);
        return [ 'success' => true ];

    } catch (Exception $e) {
        return [ 'success' => false ];
    }
}

The OrderController contains only the getUserOrders method, which passes request to the service and receives the user’s orders.

public function getUserOrders(OrderService $orderService, Request $request)
{
    return $orderService->getUserOrders($request);
}

This completes the creation of controllers in Laravel 8 for the online shop. It’s time to start creating services.

Services in Laravel 8

Creation of Services in Laravel is optional and is not described in the official documentation of the framework. But services are very convenient to use in order to offload controllers (make less code in controllers) and make your application logic clearer.

I placed the services for the shop on Laravel 8 in the app/Services folder. I needed to create 3 services to work with the corresponding entities: PizzaService, OrderService, UserService.

The first service deals with goods (pizzas). To get pizzas for the menu, a getPizzas method was created that gets all the goods and makes an array of them, convenient for the frontend.

public function getPizzas($request)
{
    $pizzas = Pizza::get();
    if ($pizzas) {
        $pizzas = $pizzas->toArray();
        foreach ($pizzas as $id => $pizza) {
            $pizzas[$id] = $this->generateSign($request, $pizzas[$id]);
        }
    }
    return $pizzas;
}

As you may have noticed, the generateSign method is also used here, which determines in which currency the prices should be displayed and recalculates them accordingly.

public function generateSign($request, $pizza)
{
    if ( isset($request->currency) && $request->currency == Pizza::CURRENCY_EUR ) {
        $pizza['price'] = number_format((float) $pizza['price'] * Pizza::CURRENCY_EUR_RATE, 2, '.', '');
        $pizza['sign'] = Pizza::CURRENCY_EUR_SIGN;
    } else {
        $pizza['sign'] = Pizza::CURRENCY_USD_SIGN;
    }
    return $pizza;
}

Also, the PizzaService service has a getPizza method for getting a single product on its page.

public function getPizza($request)
{
    $pizza = Pizza::where('id', $request->pizza_id)->first();
    if ($pizza) {
        $pizza = $pizza->toArray();
        $pizza = $this->generateSign($request, $pizza);
    }

    return $pizza;
}

The getCart method gets the products selected by the user and calculates the value of his cart.

public function getCart($request)
{
    try {
        $carts = json_decode($request->carts, true);
        $pizzas_ids = array_keys($carts);
        $pizzas = Pizza::whereIn('id', $pizzas_ids)->get();
        $subtotal = 0;
        $sign = '';
        if ($pizzas) {
            $pizzas = $pizzas->toArray();
            foreach ($pizzas as $id => $pizza) {
                if ( isset( $carts[$pizza['id']] ) ) {
                    $pizzas[$id] = $this->generateSign($request, $pizzas[$id]);

                    $sign = $pizzas[$id]['sign'];
                    $pizzas[$id]['quantity'] = $carts[ $pizza['id'] ];
                    $total_pizza_price = $pizzas[$id]['price'] * $carts[ $pizza['id'] ];
                    $pizzas[$id]['total'] = number_format((float) $total_pizza_price, 2, '.', '');
                    $subtotal += $total_pizza_price;
                }
            }
        }

        $delivery = Pizza::DELIVERY_PRICE;
        $discount = Pizza::DISCOUNT_PRICE;
        $subtotal = number_format((float) $subtotal, 2, '.', '');
        $total = number_format((float) $subtotal + $delivery - $discount, 2, '.', '');
        return [
            'success' => true,
            'pizzas' => $pizzas,
            'subtotal' => $subtotal,
            'delivery' => $delivery,
            'discount' => $discount,
            'total' => $total,
            'sign' => $sign,
        ];
    } catch (Exception $e) {
        return [
            'success' => false,
            'error' => 'Some error'
        ];
    }
}

Also in this service there are methods validateCheckout – validating data on the checkout page and getPizzasForCheckout – getting pizzas selected by the user from the database.

The OrderService service contains methods for working with orders. getUserOrders – needed to receive user orders in the user’s personal account (for authorized users).

public function getUserOrders($request)
{
    $orders = Order::where('user_id', $request->user_id)->orderByDesc('id')->get();
    if ($orders) {
        return [
            'success' => true,
            'orders' => OrderResource::collection($orders)
        ];
    }
    return [
        'success' => false,
        'error' => 'Empty orders'
    ];
}

This service also has methods calculateSubtotal – calculating the value of subtotal when creating an order, fillOrder – creating an order, generateOrderItems – to create links between an order and goods.

UserService is needed to work with users. In it, the getUserToken method gets a token for authorized users.

public function getUserToken($request)
{
    try {
        $request->user()->tokens()->delete();
        return $request->user()->createToken('token-name');
    } catch (Exception $e) {
        return null;
    }
}

GetUserForCheckout method – gets an authorized user when creating an order (if necessary).

public function getUserForCheckout($request)
{
    if ($request->is_create_account == User::CREATE_ACCOUNT) {
        $user = User::create([
            'name' => $request->firstname,
            'email' => $request->email,
            'password' => Hash::make($request->password),
        ]);
    } else if ($request->is_create_account == User::ISSET_ACCOUNT) {
        $user = User::where('email', $request->email)->first();
    } else {
        $user = null;
    }
    return $user;
}

Resources in Laravel 8

Resources in Laravel 8 are needed to modify information returned from models before sending it to the frontend. You may have already noticed resource usage in the services above. Now I will tell you what resources I needed to create an online pizza shop.

In this Laravel project, I only needed 2 resources: OrderResource and OrderPizzaResource. They are created in the folder suggested by the creators of the framework app\Http\Resources.

Within the first resource, I transform the order information displayed from the database in accordance with the currency selected by the user on the site.

class OrderResource extends JsonResource
{
    public function toArray($request)
    {
        if ( isset($request->currency) && $request->currency == Pizza::CURRENCY_EUR ) {
            $subtotal = number_format((float) $this->subtotal * Pizza::CURRENCY_EUR_RATE, 2, '.', '');
            $delivery = number_format((float) $this->delivery * Pizza::CURRENCY_EUR_RATE, 2, '.', '');
            $discount = number_format((float) $this->discount * Pizza::CURRENCY_EUR_RATE, 2, '.', '');
            $total = number_format((float) $this->total * Pizza::CURRENCY_EUR_RATE, 2, '.', '');
            $sign = Pizza::CURRENCY_EUR_SIGN;
        } else {
            $subtotal = $this->subtotal;
            $delivery = $this->delivery;
            $discount = $this->discount;
            $total = $this->total;
            $sign = Pizza::CURRENCY_USD_SIGN;
        }

        return [
            'id' => $this->id,
            'firstname' => $this->firstname,
            'lastname' => $this->lastname,
            'address' => $this->address,
            'phone' => $this->phone,
            'email' => $this->email,
            'subtotal' => $subtotal,
            'delivery' => $delivery,
            'discount' => $discount,
            'total' => $total,
            'sign' => $sign,
            'pizzas' => OrderPizzaResource::collection($this->pizzas),
        ];
    }
}

All calls like $this->firstname get the properties of the model being accessed. And the second resource OrderPizzaResource, called from the first one, gets the products in the required format and returns prices in the required currency.

class OrderPizzaResource extends JsonResource
{
    /**
     * Transform the resource into an array.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return array
     */
    public function toArray($request)
    {
        if ( isset($request->currency) && $request->currency == Pizza::CURRENCY_EUR ) {
            if ($this->pivot) {
                $price = number_format((float) $this->pivot->price * Pizza::CURRENCY_EUR_RATE, 2, '.', '');
            } else {
                $price = number_format((float) $this->price * Pizza::CURRENCY_EUR_RATE, 2, '.', '');
            }
            $count = ($this->pivot) ? $this->pivot->count : 0;
            $total = number_format((float) $price * $count, 2, '.', '');
            $sign = Pizza::CURRENCY_EUR_SIGN;
        } else {
            if ($this->pivot)
                $price = $this->pivot->price;
            else
                $price = $this->price;
            $count = ($this->pivot) ? $this->pivot->count : 0;
            $total = number_format((float) $price * $count, 2, '.', '');
            $sign = Pizza::CURRENCY_USD_SIGN;
        }

        return [
            'id' => $this->id,
            'name' => $this->name,
            'description' => $this->description,
            'price' => $price,
            'sign' => $sign,
            'image_url' => $this->image_url,
            'count' => $count,
            'total' => $total
        ];
    }
}

This completes the creation of the backend for the shop on Laravel 8, in the next part I will analyze the development of the frontend for shop on Laravel 8 in detail. We will implement it in Inertia.js (out of the box Laravel Jetstream).

Follow my Twitter not to miss it.

Share post
Twitter Facebook