Laravel Projects Examples

Food Order/Delivery Website with Vue Inertia

Example application of Restaurant Delivery system. Separate website areas for customers to place orders, for staff to track orders, and for vendors to manage menus. Built on top of Vue Inertia containing preparations for API usage (like Mobile applications).

How to install

  • Clone the repository with git clone
  • Copy the .env.example file to .env and edit database credentials there
  • Run composer install
  • Run php artisan key:generate
  • Run php artisan storage:link
  • Run php artisan migrate --seed (it has some seeded data for your testing)
  • Run npm ci and npm run build
  • Launch the main URL /. Log in with credentials for a user to desire:
  • That's it.

How It Works

The project structure allows for a WEB application and an API endpoint, for example, for a mobile app.

Service classes have been chosen to reuse code. All services are in the app/Services directory.

app/
└── Services/
├── CartService.php
├── CategoryService.php
├── OrderService.php
├── ProductService.php
├── RestaurantService.php
└── StaffMemberService.php

Controllers are divided into a directory by a user's role. APIs also have a version directory. Below, you can see a tree structure for the Controllers.

app/
└── Http/
└── Controllers/
├── Admin/
│ └── RestaurantController.php
├── Api/
│ └── V1/
│ ├── Admin/
│ │ └── RestaurantController.php
│ ├── Customer/
│ │ ├── CartController.php
│ │ └── OrderController.php
│ ├── Staff/
│ │ └── OrderController.php
│ └── Vendor/
│ ├── CategoryController.php
│ ├── ProductController.php
│ └── StaffMemberController.php
├── Auth/
│ ├── AuthenticatedSessionController.php
│ ├── ConfirmablePasswordController.php
│ ├── EmailVerificationNotificationController.php
│ ├── EmailVerificationPromtController.php
│ ├── NewPasswordController.php
│ ├── PasswordController.php
│ ├── PasswordResetLinkController.php
│ ├── RegisteredUserController.php
│ └── VerifyEmailController.php
├── Customer/
│ ├── CartController.php
│ └── OrderController.php
├── Staff/
│ └── OrderController.php
├── Vendor/
│ ├── CategoryController.php
│ ├── MenuController.php
│ ├── ProductController.php
│ └── StaffMemberController.php
├── Controller.php
├── HomeController.php
├── ProfileController.php
└── RestaurantController.php

API Controllers return an API resource, which can be found in the app/Http/Resources directory.

app/
└── Http/
└── Resources/
└── Api/
└── V1/
├── Admin/
│ ├── RestaurantCollection.php
│ └── RestaurantResource.php
├── Customer/
│ ├── OrderCollection.php
│ └── OrderResource.php
├── Staff/
│ ├── OrderCollection.php
│ └── OrderResource.php
└── Vendor/
├── CategoryCollection.php
├── CategoryResource.php
├── ProductCollection.php
├── ProductResource.php
├── StaffMemberCollection.php
└── StaffMemberResource.php

Authorization is done on the backend and frontend. First, permissions are seeded to the database.

database/seeders/PermissionsSeeder.php:

use App\Models\Permission;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
 
class PermissionSeeder extends Seeder
{
public function run(): void
{
$actions = [
'viewAny',
'view',
'create',
'update',
'delete',
'restore',
'forceDelete',
];
 
$resources = [
'user',
'restaurant',
'category',
'product',
'order',
];
 
collect($resources)
->crossJoin($actions)
->map(function ($set) {
return implode('.', $set);
})->each(function ($permission) {
Permission::create(['name' => $permission]);
});
 
Permission::create(['name' => 'cart.add']);
}
}

Then, logged-in users' permissions are registered as gates in the AppServiceProvider.

app/Providers/AppServiceProvider.php:

use App\Models\Permission;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\ServiceProvider;
 
class AppServiceProvider extends ServiceProvider
{
// ...
 
public function boot(): void
{
$this->registerGates();
}
 
protected function registerGates(): void
{
try {
foreach (Permission::pluck('name') as $permission) {
Gate::define($permission, function ($user) use ($permission) {
return $user->hasPermission($permission);
});
}
} catch (\Exception $e) {
info('registerPermissions(): Database not found or not yet migrated. Ignoring user permissions while booting app.');
}
}
}

In the backend, permissions are checked using a Gate facade. Below is an example from a Controller for the admin user.

app/Http/Controllers/Admin/RestaurantController.php:

use Illuminate\Auth\Access\Gate;
use App\Http\Controllers\Controller;
use App\Models\Restaurant;
use App\Services\RestaurantService;
use Inertia\Inertia;
use Inertia\Response;
 
class RestaurantController extends Controller
{
public function __construct(public RestaurantService $restaurantService)
{
}
 
public function index(): Response
{
Gate::authorize('restaurant.viewAny');
 
return Inertia::render('Admin/Restaurants/Index', [
'restaurants' => Restaurant::with(['city', 'owner'])->get(),
]);
}
 
// ...
}

Permissions are passed to the front end using the Inertia shared data feature from the Middleware. Other information like the user, if user is a vendor, status for flash message, and the cart are added here. All these shared values can be accessed in the frontend from usePage props.

app/Http/Middleware/HandleInertiaRequests.php:

use App\Services\CartService;
use Illuminate\Http\Request;
use Inertia\Middleware;
 
class HandleInertiaRequests extends Middleware
{
// ...
 
public function share(Request $request): array
{
return array_merge(parent::share($request), [
'auth' => [
'user' => $request->user(),
'permissions' => $request->user()?->permissions() ?? [],
'is_vendor' => $request->user()?->isVendor(),
],
'status' => session('status'),
'cart' => (new CartService())->all(),
]);
}
}

A mixin is created for the frontend to have a can() function, which accepts a permissions name and returns a true/false if the user has such permission.

resources/js/Support/can.js:

import { usePage } from '@inertiajs/vue3'
 
export const Can = {
install: (v) => {
const page = usePage()
 
const can = (permission) => {
return page.props.auth.permissions.includes(permission)
}
 
v.mixin({
methods: { can }
})
}
}

Below is an example of usage to show the edit button only if the user has permission to edit the record.

resources/js/Pages/Admin/Restaurants/Index.vue:

<script setup>
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue'
import { Head, Link } from '@inertiajs/vue3'
 
defineProps({
restaurants: {
type: Array
}
})
</script>
 
<template>
// ...
<Link
v-if="can('restaurant.update')"
:href="route('admin.restaurants.edit', restaurant)"
class="btn btn-secondary"
>
Edit
</Link>
// ...
</template>

We use notifications to send emails. This application has three notifications: NewOrderCreated, RestaurantOwnerInvitation, and RestaurantStaffInvitation.

app/
└── Notifications/
├── NewOrderCreated.php
├── RestaurantOwnerInvitation.php
└── RestaurantStaffInvitation.php

Below is an example of one of the notifications.

app/Notifications/NewOrderCreated.php:

use App\Models\Order;
use App\Models\Restaurant;
use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
 
class NewOrderCreated extends Notification
{
use Queueable;
 
protected Order $order;
 
protected Restaurant $restaurant;
 
protected Collection $products;
 
protected User $customer;
 
public function __construct(Order $order)
{
$this->order = $order;
$this->restaurant = $order->restaurant;
$this->products = $order->products;
$this->customer = $order->customer;
}
 
public function via(object $notifiable): array
{
return ['mail'];
}
 
public function toMail(object $notifiable): MailMessage
{
return (new MailMessage)
->subject(__('[:restaurant_name] New Order', [
'restaurant_name' => $this->restaurant->name,
]))
->markdown('mail.order.new-order-created', [
'order' => $this->order,
'restaurant' => $this->restaurant,
'products' => $this->products,
'customer' => $this->customer,
]);
}
}

app/Services/OrderService.php:

use App\Enums\OrderStatus;
use App\Models\Order;
use App\Models\User;
use App\Notifications\NewOrderCreated;
use Illuminate\Support\Facades\DB;
 
class OrderService
{
// ...
 
public function placeOrder(User $user, array $attributes): Order
{
$order = DB::transaction(function () use ($user, $attributes) {
$order = $user->orders()->create([
'restaurant_id' => $attributes['restaurant_id'],
'total' => $attributes['total'],
'status' => OrderStatus::PENDING,
]);
 
$order->products()->createMany($attributes['items']);
 
return $order;
});
 
$order->restaurant->owner->notify(new NewOrderCreated($order));
 
$this->cart->flush();
 
return $order;
}
 
// ...
}

The Frontend Vue components for the pages are in the standard resources/js/Pages directory. As with Controllers, everything is divided into sub-directories.

resources/
└── js/
└── Pages/
├── Admin/
│ └── Restaurants/
│ ├── Create.vue
│ ├── Edit.vue
│ └── Index.vue
├── Auth/
│ ├── ConfirmPassword.vue
│ ├── ForgotPassword.vue
│ ├── Login.vue
│ ├── Register.vue
│ ├── ResetPassword.vue
│ └── VerifyEmail.vue
├── Customer/
│ ├── Cart.vue
│ └── Orders.vue
├── Profile/
│ ├── Partials/
│ │ ├── DeleteUserForm.vue
│ │ ├── UpdatePasswordForm.vue
│ │ └── UpdateProfileInformationForm.vue
│ └── Edit.vue
├── Staff/
│ └── Orders.vue
├── Vendor/
│ ├── Categories/
│ │ ├── Create.vue
│ │ ├── Edit.vue
│ │ └── Index.vue
│ ├── Products/
│ │ ├── Create.vue
│ │ └── Edit.vue
│ ├── Staff/
│ │ ├── Partials/
│ │ │ ├── AddStaffMemberForm.vue
│ │ │ └── StaffMemberManager.vue
│ │ └── Show.vue
│ └── Menu.vue
├── Home.vue
└── Restaurant.vue