Laravel as backend
Inertia as Laravel-Vue.js adapter
Vue.js as frontend
Quasar.js for Vue.js (component library & basic styling)
Laravel serves as the backend. It is mainly responsible for:
- Routing
- User authentication & session handling
- Database queries
Inertia serves as an adapter for Laravel and Vue. It is mainly responsible for:
- Passing data as JSON to update Vue components/switching them out
- making Laravel routes accessible the frontend
- making basic form requests to the backend
Vue.js together is for building the main user interface. It is used for:
-
highly interactive user interfaces
-
seamless Single Page Application experience
Quasar can be used as a complete frontend framework build on Vue.js or just a component library.
- Wide range of components such as buttons, img, card, etc. ...
- page layouting
- basic styling with grid and flexbox pattern
A brief overview of the most relevant files and folders.
|-- smartphoneportal_jannis
|-- app
| |-- Http
| | |-- Controllers
| | |-- Middleware
| | |-- Models
| |-- database
| |-- factories
| |-- migrations
| |-- seeders
|-- resources
| |-- css
| |-- js
| | |-- Components
| | |-- Pages
| | |-- Layouts
| |-- views
|-- routes
/app | holds the core code of laravel |
---|---|
/Http/Controllers | holds request handlers, mostly for the laravel models User => UserController |
/Http/Middleware | holds classes for request filtering |
/Http/Models | holds classes based on eloquent ORM models, wich reflect each a database table |
/database | |
---|---|
/factories | holds factory classes for the models |
/migrations | holds the database schemas for each table |
/seeders | holds files wich can be used to apply some sort of initial data to the database |
/resources | |
---|---|
/css | sass and css styles |
/js | main Vue.js installation |
/js/Components | all universal Vue.js components such as "ProductCart" |
/js/Pages | main pages |
/js/Layouts | layout files for the pages |
/views | holds the app.blade.php wich is sort of the source file of all views |
Tablename | Purpose | Notes |
---|---|---|
smartphones | Contains the most important information about a smartphone model | |
smartphone_prices | contains the price information about smartphones | at registered least one per smartphone: default 0€ |
smartphone_images | contains all images available for a smartphone | |
brands | contains information about all smartphone brands | |
currencies | contains all available currencies | |
ratings | for the rating of smartphones by users with the 5-star rating system | |
users | basic information about registered users | |
comments | stores comments either for smartphones or for themselves | a polymorphic relationship |
likes | stores comment ratings based on Like/Dislike per user | user can only 'Like' OR 'Dislike' the same comment |
|-- smartphoneportal_jannis
|-- app
| |-- Http
| | |-- Models
By default, all Laravel Models inherit the database columns as a public attribute. To now set the intended relationships for the models, they need to be specified here so eloquent can automatically build the join queries for you.
class Smartphone extends Model
{
...
public function brand(): BelongsTo
{
return $this->belongsTo(Brand::class);
}
...
}
To complete the relation, the counterpart should also be defined within the Brand::class
.
class Brand extends Model
{
public function smartphones(): HasMany
{
return $this->hasMany(Smartphone::class);
}
}
Now, all that needs to be done to retrieve all related smartphones for one brand is to call this function like an attribute on a single brand model instance. The rest will be handled for you. The relation will not get loaded by default!
class SomeController extends Controller
{
public function getRelatedSmartphones(): Smartphone[]|Collection
{
return Brand::first()->smartphones;
}
}
|-- smartphoneportal_jannis
|-- app
| |-- Http
| | |-- Controllers
|-- routes
The Controller is not explicitly bound to a Laravel model and is generally just a request handler. But for grouping several actions associated with a model you typically would create a ModelController.
To specify which controller and function should be handling a request, you need to define this to your web routes.
Route::get('/', [SmartphoneController::class, 'index']);
Now every get request to the base URL smartphoneportal.com
would be handeld by Controller and the index function.
In the example below the controller returns an Inertia response, meaning either the resources/views/app.blade.php
where the initial Inertia-Vue.js app is included or if the app is already instantiated at the client, a JSON response to
update the Vue components and props. Here it would return a paginate JSON object of 16 smartphones
that are not
Discontinued or Cancelled, with the related data of brand, prices, and the price currency (a nested relationship).
Additionally the avg value of the related rating values and the total rating count of each model.
public function index(): Response
{
$smartphones = Smartphone::where('launch_status', '!=', 'Discontinued')
->where('launch_status', '!=', 'Cancelled')
->with(['brand', 'smartphonePrices', 'smartphonePrices.currency'])
->orderBy('featured', 'DESC')
->paginate(16);
$smartphones->loadAvg('ratings', 'stars');
$smartphones->loadCount('ratings');
return Inertia::render('Shop/Index', ['smartphones' => $smartphones]);
}
Once a middleware is defined globally, for a group within app/Http/Kernel.php
or a route, they will filter the
incoming request.
The example for the post route smartphoneportal.test/smartphone/99/rating/update
within routes/web.php
. To prevent
non-users and non-verified users to be able to rate smartphones, the middlewares auth
and verified
are applied to
that route, passed as an array to the Route::middleware()
.
Route::middleware(['auth', 'verified'])->group(function () {
Route::post('/smartphone/{smartphone}/rating/update', [RatingController::class, 'update'])
->name('smartphone.rating.update');
}
The seeders are for filling the database with initial and/or required data. They manually insert some small chunk of
data (eg. the main test user) or outsource the actual work to the factories to create easily large amounts of dummy
data. The DatabaseSeeder
is sort of the "main" seeding class, which is automatically called with the artisan command
when setting up a fresh database with php artisan migrate:fresh --seed
. Within a seeder, you can additionally call
other seeder classes.
public function run(): void
{
User::factory()->createOne(['name' => 'test', 'email' => 'test@mail.de']);
$this->call([
SmartphoneSeeder::class,
UserSeeder::class
]);
}
In general, they are a great way to create dummy data together, especially combined with the database seeders.
The smartphone factory is mainly responsible for importing smartphone data via the
provided database/seeders/files/smartphones.csv
file. The $this->smartphoneMapping
contains the mapping of for the
csv collumns to model attributes (database collumns) [csv index => model attribute]
.
public function createFromCsv(): void
{
while (($csvRow = fgetcsv($file)) !== false) {
$smartphone = Smartphone::create(); //creates a new empty smartphone model
foreach ($this->smartphoneMapping as $key => $colName) {
...
$smartphone->setAttribute($colName, $csvRow[$key]);
}
}
}
The only view file resources/views/app.blade.php
is reposible for the inital response to deliver everything
necessary, such as css stylesheet, fonts, csrf token, the routes for Ziggy.js (for using larval routes within js
frontend) and the inertia app together with vue.
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title inertia>{{ config('app.name', 'smartphoneportal') }}</title>
<link rel="stylesheet" href="{{ mix('css/app.css') }}">
@routes
<script src="{{ mix('js/app.js') }}" defer></script>
</head>
<body class="page-font">
@inertia
@env ('local')
<script src="http://localhost:3000/browser-sync/browser-sync-client.js"></script>
@endenv
</body>
</html>
The routes/web.php
is for assigning actions to web routes. You can directly handle the route or point to one of your
Controller classes. Here you can also name the routes and assign middlewares to them.
Route::get('/dashboard', function () {
return Inertia::render('Dashboard');
})->middleware(['auth', 'verified'])->name('dashboard');
Route::get('/', [SmartphoneController::class, 'index'])->name('index');
Thanks to the Ziggy.js library the named routes can also be used within the frontend. Here shown within the
inertia Link
component.
<Link :href = "route('phone.show', smartphone.id)" label = "dummy" />
The frontend is based on Vue.js but within its configuration resources/js/app.js
it is wrapped into inertia which
contains the main Vue app. Inertia is for intercepting the request and responses and therefore updates the component
props or re-renders components, based on the Inertia::render()
specific response from the backend.
The smartphone table originates from this 'database/seeders/files/smartphones.csv'
file if found
online here. It contains scraped data from the website
GSMArena, which holds reviews and phone information. The .csv holds about 10.105 unique smartphones. The data within the
misc_prices column has some wrong encoded UTF-8 hex characters such as <e2><82><ac>
for the €
sign. SmartphoneFactory::createFromCsv()
is responsible for seeding the data from the smartphones.csv
file.