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
andnpm run build
- Launch the main URL
/
. Log in with credentials for a user to desire:- Admin: [email protected] / password
- Vendor: [email protected] / password
- Customer: [email protected] / password
- 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