- LARAVEL APACHE SETUP
CREATE A CONF
1sudo cp /etc/apache2/sites-available/000-default.conf /etc/apache2/sites-available/abilik-laravel.test.conf
EDIT
1sudo gedit /etc/apache2/sites-available/abilik-laravel.test.conf
CONTENT
123456789101112<VirtualHost abilik-laravel.test:80>ServerName www.abilik-laravel.testDocumentRoot "/home/teddy/Documents/works/abilik-laravel"DirectoryIndex index.php<Directory "/home/teddy/Documents/works/abilik-laravel">AllowOverride AllAllow from AllRequire all granted</Directory>ErrorLog /var/log/apache2/abilik-laravel.error.logCustomLog /var/log/apache2/abilik-laravel.access.log combined</VirtualHost>
ENABLE THE VIRTUALHOST
123sudo a2ensite abilik-laravel.test.confsudo a2enmod rewrite
RELOAD APACHE2
1sudo systemctl reload apache2CREATE THE HOST
1sudo gedit /etc/hosts1127.0.0.1 abilik-laravel.testSET THE FILE PERMISSIONS & RELOAD APACHE2
12teddy@teddy:~/Documents/works$ sudo chmod -R 777 abilik-laravel/teddy@teddy:~/Documents/works$ sudo systemctl reload apache2MODIFY .env FILE
123DB_DATABASE=abilik_laravelDB_USERNAME=rootDB_PASSWORD=teddyCHECK /home/teddy/Documents/works/abilik-laravel/app/Providers/AppServiceProvider.php
TO DISABLE HTTPS ON MY LOCAL1234public function boot(){//\URL::forceScheme('https');}MODIFY .htaccess (DEFAULT FROM /public/ DIR)
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647AddType application/x-httpd-php .phpAddType application/x-httpd-php-source .phps<IfModule mod_rewrite.c><IfModule mod_negotiation.c>Options -MultiViews -Indexes</IfModule>RewriteEngine On# Handle Authorization HeaderRewriteCond %{HTTP:Authorization} .RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]# Redirect Trailing Slashes If Not A Folder...RewriteCond %{REQUEST_FILENAME} !-dRewriteCond %{REQUEST_URI} (.+)/$RewriteRule ^ %1 [L,R=301]# Handle Front Controller...RewriteCond %{REQUEST_FILENAME} !-dRewriteCond %{REQUEST_FILENAME} !-fRewriteRule ^ index.php [L]</IfModule><IfModule mod_headers.c>Header append Cache-Control "public"</IfModule>## EXPIRES CACHING ##<IfModule mod_expires.c>ExpiresActive OnExpiresByType image/jpg "access plus 1 year"ExpiresByType image/jpeg "access plus 1 year"ExpiresByType image/gif "access plus 1 year"ExpiresByType image/png "access plus 1 year"ExpiresByType text/css "access plus 1 month"ExpiresByType application/pdf "access plus 1 month"ExpiresByType text/x-javascript "access plus 20 minutes"ExpiresByType application/x-shockwave-flash "access plus 1 month"ExpiresByType application/x-javascript "access plus 20 minutes"ExpiresByType text/javascript "access plus 15 minutes"ExpiresByType application/javascript "access plus 1 month"ExpiresByType application/json "access plus 20 minutes"ExpiresByType image/x-icon "access plus 1 year"ExpiresDefault "access plus 2 days"</IfModule>## EXPIRES CACHING ##CLEAR THE PREVIOUS CACHE & ROUTE ON THE WEB ROOT (FOR EXAMPLE teddy@teddy:~/Documents/works/abilik-laravel$ php artisan route:clear)
12345php artisan route:clearphp artisan config:clearphp artisan cache:clear123teddy@teddy:~/Documents/works/abilik-laravel$ php artisan help -VPHP Warning: Module 'curl' already loaded in Unknown on line 0Laravel Framework 6.5.1OPEN: http://abilik-laravel.test/
http://abilik-laravel.test/oc-admin
Category: Laravel
Laravel and Swoole
Ref: https://github.com/huang-yi/laravel-swoole-http
My setting:
|
1 2 3 4 5 |
php version: PHP 7.3.9-1+ubuntu16.04.1+deb.sury.org+1 (cli) (built: Sep 2 2019 12:54:04) ( NTS ) Copyright (c) 1997-2018 The PHP Group Zend Engine v3.3.9, Copyright (c) 1998-2018 Zend Technologies Swoole version: 4.4.5 |
Create a laravel project. I tried version 6.x but I can’t use ‘laravel-swoole-http’ so I installed version 5.6.x
|
1 |
composer create-project --prefer-dist laravel/laravel laravel-swoole "5.6.*" |
Then go to the dir ‘laravel-swoole’ then install laravel-swoole-http’ with ‘composer require huang-yi/laravel-swoole-http’
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
composer require huang-yi/laravel-swoole-http Using version ^3.1 for huang-yi/laravel-swoole-http ./composer.json has been updated Loading composer repositories with package information Updating dependencies (including require-dev) Package operations: 2 installs, 0 updates, 0 removals - Installing huang-yi/swoole-watcher (0.0.2): Downloading (100%) - Installing huang-yi/laravel-swoole-http (3.1.7):Downloading (100%) ) Writing lock file Generating optimized autoload files Carbon 1 is deprecated, see how to migrate to Carbon 2. https://carbon.nesbot.com/docs/#api-carbon-2 You can run './vendor/bin/upgrade-carbon' to get help in updating carbon and other frameworks and libraries that depend on it. > Illuminate\Foundation\ComposerScripts::postAutoloadDump > @php artisan package:discover Discovered Package: fideloper/proxy Discovered Package: huang-yi/laravel-swoole-http Discovered Package: laravel/tinker Discovered Package: nunomaduro/collision Package manifest generated successfully. |
then register/publish the swoole service
|
1 2 3 4 |
php artisan vendor:publish --provider="HuangYi\Swoole\SwooleServiceProvider" Copied File [/vendor/huang-yi/laravel-swoole-http/config/swoole.php] To [/config/swoole.php] Publishing complete. |
then start swoole server
|
1 2 3 4 |
php artisan swoole:server Starting server... > (Run this command to ensure the server process is running: ps -ef|grep "{app.name}") |
open it on your browser: http://127.0.0.1:1215/
the setting in laravel-swoole/config/swoole.php like host (localhost), port (1215), etc
TO stop the swoole server:
|
1 2 3 4 |
php artisan swoole:server stop Stopping server... > success |
ANOTHER ACTIVE LARAVEL SWOOLE (laravel-swooletw)
https://github.com/swooletw/laravel-swoole
PLS READ https://github.com/swooletw/laravel-swoole/wiki
Create a laravel project. I installed version 5.6.x
|
1 |
composer create-project --prefer-dist laravel/laravel laravel-swooletw "5.6.*" |
Then go to the dir ‘laravel-swooletw’ then install laravel-swoole’ with ‘composer require swooletw/laravel-swoole’
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
composer require swooletw/laravel-swoole Using version ^2.6 for swooletw/laravel-swoole ./composer.json has been updated Loading composer repositories with package information Updating dependencies (including require-dev) Package operations: 2 installs, 0 updates, 0 removals - Installing predis/predis (v1.1.1): Downloading (100%) - Installing swooletw/laravel-swoole (v2.6.64): Downloading (100%) predis/predis suggests installing ext-phpiredis (Allows faster serialization and deserialization of the Redis protocol) Writing lock file Generating optimized autoload files Carbon 1 is deprecated, see how to migrate to Carbon 2. https://carbon.nesbot.com/docs/#api-carbon-2 You can run './vendor/bin/upgrade-carbon' to get help in updating carbon and other frameworks and libraries that depend on it. > Illuminate\Foundation\ComposerScripts::postAutoloadDump > @php artisan package:discover Discovered Package: fideloper/proxy Discovered Package: laravel/tinker Discovered Package: nunomaduro/collision Discovered Package: swooletw/laravel-swoole Package manifest generated successfully. |
from https://github.com/swooletw/laravel-swoole/wiki/4.-Installation,I need to register the swoole service provider in config/app.php
|
1 2 3 4 5 6 7 |
... 'providers' => [ ... SwooleTW\Http\LaravelServiceProvider::class, ], |
Then need to publish the configuration
|
1 2 3 4 5 6 |
php artisan vendor:publish --tag=laravel-swoole Copied File [/vendor/swooletw/laravel-swoole/config/swoole_http.php] To [/config/swoole_http.php] Copied File [/vendor/swooletw/laravel-swoole/config/swoole_websocket.php] To [/config/swoole_websocket.php] Copied File [/vendor/swooletw/laravel-swoole/routes/websocket.php] To [/routes/websocket.php] Publishing complete. |
Then run the server :
|
1 |
php artisan swoole:http start |
open it on your browser: http://localhost:1215/
ANOTHER LARAVEL – SWOOLE IS
https://github.com/hhxsv5/laravel-s
I download for auction app from https://github.com/GallopYD/domain-spider
STEPS:
|
1 2 3 4 5 |
git clone https://github.com/GallopYD/domain-spider laravel-auction cd laravel-auction composer install |
CREATE A .env file:
|
1 2 3 4 5 6 7 8 9 10 11 |
APP_ENV=local APP_DEBUG=true APP_KEY=base64:CVSEtDyAtoBbQbZ5udq3vbrM8+URejsZSOFwqA7dBAE= DB_HOST=localhost DB_DATABASE=laravel_auction_swoole DB_USERNAME=root DB_PASSWORD=teddy CACHE_DRIVER=file SESSION_DRIVER=file |
CREATE A NEW DATABASE ‘laravel_auction_swoole’ COLLATION : utf8mb4_unicode_ci
RUN
|
1 2 3 4 5 6 7 8 9 |
teddy@teddy:~/Documents/works/swoole/laravel-auction$ php artisan help -V Laravel Framework 5.6.39 teddy@teddy:~/Documents/works/swoole/laravel-auction$ php artisan key:generate Application key [base64:CVSEtDyAtoBbQbZ5udq3vbrM8+URejsZSOFwqA7dBAE=] set successfully. teddy@teddy:~/Documents/works/swoole/laravel-auction$ php artisan migrate --seedMigration table created successfully. Migrating: 2019_08_11_111354_create_domains_table Migrated: 2019_08_11_111354_create_domains_table |
READ https://github.com/hhxsv5/laravel-s
PUBLISH
|
1 2 3 4 5 6 7 8 9 |
teddy@teddy:~/Documents/works/swoole/laravel-auction$ php artisan laravels publish /home/teddy/Documents/works/swoole/laravel-auction/config/laravels.php already exists, do you want to override it ? Y/N [N]: > Y Copied file [/home/teddy/Documents/works/swoole/laravel-auction/vendor/hhxsv5/laravel-s/config/laravels.php] To [/home/teddy/Documents/works/swoole/laravel-auction/config/laravels.php] Linked file [/home/teddy/Documents/works/swoole/laravel-auction/vendor/hhxsv5/laravel-s/bin/laravels] To [/home/teddy/Documents/works/swoole/laravel-auction/bin/laravels] Linked file [/home/teddy/Documents/works/swoole/laravel-auction/vendor/hhxsv5/laravel-s/bin/fswatch] To [/home/teddy/Documents/works/swoole/laravel-auction/bin/fswatch] Linked file [/home/teddy/Documents/works/swoole/laravel-auction/vendor/hhxsv5/laravel-s/bin/inotify] To [/home/teddy/Documents/works/swoole/laravel-auction/bin/inotify] |
START SERVER
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
teddy@teddy:~/Documents/works/swoole/laravel-auction$ php bin/laravels start _ _ _____ | | | |/ ____| | | __ _ _ __ __ ___ _____| | (___ | | / _` | '__/ _` \ \ / / _ \ |\___ \ | |___| (_| | | | (_| |\ V / __/ |____) | |______\__,_|_| \__,_| \_/ \___|_|_____/ Speed up your Laravel/Lumen >>> Components +---------------------------+--------------------------------------+ | Component | Version | +---------------------------+--------------------------------------+ | PHP | 7.3.9-1+ubuntu16.04.1+deb.sury.org+1 | | Swoole | 4.4.5 | | LaravelS | 3.5.10 | | Laravel Framework [local] | 5.6.39 | +---------------------------+--------------------------------------+ >>> Protocols +-----------+--------+-------------------+----------------+ | Protocol | Status | Handler | Listen At | +-----------+--------+-------------------+----------------+ | Main HTTP | On | Laravel Framework | 127.0.0.1:5200 | +-----------+--------+-------------------+----------------+ >>> Feedback: https://github.com/hhxsv5/laravel-s [2019-09-21 16:54:46] [TRACE] Swoole is running, press Ctrl+C to quit. |
OPEN IT ON THE BROWSER : http://localhost:5200/
Create Laravel Store with Vue
Ref: https://blog.pusher.com/ecommerce-laravel-vue-part-1/
In this article, we will cover how you can use Laravel to build a simple e-commerce application. After this tutorial, you should know how to use Laravel and Vue to make a web application and understand the basics of making an online store.
With the advent of the internet, many regular things became faster and easier. One of the things that improved is commerce. Commercial activities carried out on the web are known as e-commerce.
E-commerce applications make sales of goods and services over the internet possible. If you use the internet often, chances are you have come across one or even used one at some point.
Introduction
As we mentioned earlier, an e-commerce application makes selling goods and services online very convenient. It provides a listing of the products the seller wishes to sell showing their prices, then a page where you can see all the details of the single product selected, and finally, where to pay for the product and set how you wish to receive it.
With this in mind, we know that to have a useful e-commerce application, we’d need to develop the following:
A listing page to view all our products.
A single page to view product information.
A checkout page where the buyer can enter a delivery address and pay.
A simple dashboard for a buyer to see all purchased products, and know if they have shipped.
A way for the seller to add products to sell.
A way for the seller to see which orders users placed.
A way for the seller to show a product has been shipped to the user.
A way for the seller to see and update all product information.
Planning the structure of the application
From our breakdown above, you may see that this application will have two user types:
Administrator – The owner of the store.
Buyer – The person who wants to buy a gift for a friend.
You definitely know that the application will also have products that need to be stored. These products will be ordered and you need a way to store and track those orders. These would form the basis of any e-commerce platform.
The basic details we need to store in the different parts:
|
1 2 3 4 |
Table Fields User name, email, password, is_admin Product name, description, price, units, image Order product, user, quantity, address, is_delivered |
The basic operations we need the app to carry out, and where these are handled, are:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
Operation Controller Login UserController Register UserController User profile UserController View all orders by a single user UserController View product listing ProductController View a single product ProductController Edit a product ProductController Add a new product ProductController Add more units to a product ProductController Remove a product ProductController Order product OrderController View all orders OrderController View a single order information OrderController Deliver an order OrderController Delete an order OrderController |
If you want to go a little deeper, you can consider adding categories, tags, warehouse items, more user types and many other things. However, we will not be considering them for this guide, but you should definitely try it on your own.
A good way to learn is by practicing. You can exercise your mind by doing the task in the previous paragraph.
Creating the application
We are going to use the Laravel CLI to make a new Laravel application. To create a new Laravel application, run the following command:
|
1 2 3 4 5 6 7 8 9 |
teddy@teddy:~/Documents/works/laravel$ laravel new laravel-store Crafting application... Loading composer repositories with package information Installing dependencies (including require-dev) from lock file Package operations: 86 installs, 0 updates, 0 removals - Installing doctrine/inflector (v1.3.0): Loading from cache ... Package manifest generated successfully. Application ready! Build something amazing. |
Then you can cd into the project we just created. The Laravel related commands we run throughout the article needs to be run from the root of the Laravel project.
|
1 2 |
teddy@teddy:~/Documents/works/laravel$ cd laravel-store/ teddy@teddy:~/Documents/works/laravel/laravel-store$ |
Creating the models for the application
Models in Laravel provide a very convenient way to interact with the database. They provide methods for all the basic operations you need to run for your database table.
Let’s make the models that will interact with our database and hold business logic. New Laravel installations come with the User model out of the box, which can be found in app directory so let’s make the other two models:
|
1 2 3 4 |
teddy@teddy:~/Documents/works/laravel/laravel-store$ php artisan make:model Product -mr Model created successfully. Created Migration: 2019_01_31_134200_create_products_table Controller created successfully. |
We have added the -mr flag to the make:model command so that it will generate the accompanying migration and controller for the model.
|
1 2 3 4 |
teddy@teddy:~/Documents/works/laravel/laravel-store$ php artisan make:model Order -mr Model created successfully. Created Migration: 2019_01_31_134258_create_orders_table Controller created successfully. |
Next open the app/User.php file and replace the contents with the following:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
//app/User.php namespace App; use Illuminate\Notifications\Notifiable; use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Foundation\Auth\User as Authenticatable; class User extends Authenticatable { use Notifiable, SoftDeletes; protected $fillable = [ 'name', 'email', 'password', ]; protected $hidden = [ 'password', 'remember_token', ]; public function orders() { return $this->hasMany(Order::class); } } |
We are using SoftDeletes to allow us to mark a database record as deleted without actually deleting it completely. This is useful if you want to be able to restore the data.
We also have an array – $fillable, with the column names we want to mass assign on the users table. Mass assignment happens when we call our User model statically and pass an array to its create method.
Open app/Product.php file and edit as follows:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
//app/Product.php namespace App; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\SoftDeletes; class Product extends Model { use SoftDeletes; protected $fillable = [ 'name', 'price', 'units', 'description', 'image' ]; public function orders(){ return $this->hasMany(Order::class); } } |
The Product model is quite similar to the User model. It has the $fillable array and also an orders method for establishing a relationship with orders placed on the application.
Now, open app/Order.php file and edit as follows:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
//app/Order.php namespace App; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\SoftDeletes; class Order extends Model { use SoftDeletes; protected $fillable = [ 'product_id', 'user_id', 'quantity', 'address' ]; public function user() { return $this->belongsTo(User::class, 'user_id'); } public function product() { return $this->belongsTo(Product::class, 'product_id'); } } |
The Order model looks slightly different from the other two but is essentially the same thing. We just established a different kind of relationship – belongsTo that shows which user made an order or which product was ordered.
Defining the migrations for the application
Migrations are a good way to create and maintain your application’s database. It essentially defines how tables should be created or modified.
Migrations are useful because they help you manage the database tables, columns and keys. You can share migration files instead of raw SQL, and because migration files are run chronologically, they make it easy to work with git, so it’s great for teams.
Open the create_users_table migrations file in the database/migrations directory and replace the content with the following:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
//database/migrations/2014_10_12_000000_create_users_table.php use Illuminate\Support\Facades\Schema; use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Migrations\Migration; class CreateUsersTable extends Migration { public function up() { Schema::create('users', function (Blueprint $table) { $table->increments('id'); $table->string('name'); $table->string('email')->unique(); $table->boolean('is_admin')->default(false); $table->string('password'); $table->rememberToken(); $table->timestamps(); $table->softDeletes(); }); } public function down() { Schema::dropIfExists('users'); } } |
Laravel is quite readable and you can probably figure out what is going on by reading the code. We defined which columns should exist in the table and their attributes.
There are many methods in the Blueprint class for migrations. You can read more here.
Next, open the create_products_table migrations file in the database/migrations directory and replace the code with the following:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
//database/migrations/2019_01_31_134200_create_products_table.php use Illuminate\Support\Facades\Schema; use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Migrations\Migration; class CreateProductsTable extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::create('products', function (Blueprint $table) { $table->increments('id'); $table->string('name'); $table->string('description'); //$table->integer('units')->unsigned()->default(0); $table->unsignedInteger('units')->default(0); $table->double('price'); $table->string('image'); $table->timestamps(); $table->softDeletes(); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::dropIfExists('products'); } } |
Finally, open the create_orders_table migrations file in the database/migrations directory and replace the contents with the following:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
//database/migrations/2019_01_31_134258_create_orders_table.php use Illuminate\Support\Facades\Schema; use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Migrations\Migration; class CreateOrdersTable extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::create('orders', function (Blueprint $table) { $table->increments('id'); $table->unsignedInteger('product_id'); $table->unsignedInteger('user_id'); $table->unsignedInteger('quantity')->default(1); $table->string('address')->nullable(); $table->boolean('is_delivered')->default(false); $table->timestamps(); $table->softDeletes(); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::dropIfExists('orders'); } } |
Creating seeders for the application
Seeders are an excellent way to pre-populate our database with dummy data. We are going to use the seeder class to create the user account for our administration.
Create a seeder class by running the command below:
|
1 2 |
teddy@teddy:~/Documents/works/laravel/laravel-store$ php artisan make:seed UsersTableSeeder Seeder created successfully. |
Now, open the file UserTableSeeder.php at the database/seeds directory and replace the content with the following:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
//database/seeds/UsersTableSeeder.php use Illuminate\Database\Seeder; use App\User; class UsersTableSeeder extends Seeder { /** * Run the database seeds. * * @return void */ public function run() { $user = new User; $user->name = "Admin"; $user->email = "admin@admin.com"; $user->password = bcrypt('secret'); $user->is_admin = true; $user->save(); } } |
The seeder class above will create a new admin user in the database.
Recall that when we defined the User model, we did not include is_admin in the fillable column. The reason is that we do not want anyone who tries to spoof our application to create an administrator user. This is why we had to create a user instance here so we can access all the columns from the user table.
Make another seeder class for our products table:
|
1 2 |
teddy@teddy:~/Documents/works/laravel/laravel-store$ php artisan make:seed ProductsTableSeeder Seeder created successfully. |
Open the database/seeds/ProductTableSeeder.php file and replace the contents with the following:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
//database/seeds/ProductsTableSeeder.php use Illuminate\Database\Seeder; class ProductsTableSeeder extends Seeder { /** * Run the database seeds. * * @return void */ public function run() { $products = [ [ 'name' => "MEN'S BETTER THAN NAKED & JACKET", 'description' => 'Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua consequat.', 'units' => 21, 'price' => 200.10, 'image' => 'http://images.thenorthface.com/is/image/TheNorthFace/236x204_CLR/mens-better-than-naked-jacket-AVMH_LC9_hero.png', 'created_at' => new DateTime, 'updated_at' => null, ], [ 'name' => "WOMEN'S BETTER THAN NAKED™ JACKET", 'description' => 'Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua consequat.', 'units' => 400, 'price' => 1600.21, 'image' => 'http://images.thenorthface.com/is/image/TheNorthFace/236x204_CLR/womens-better-than-naked-jacket-AVKL_NN4_hero.png', 'created_at' => new DateTime, 'updated_at' => null, ], [ 'name' => "WOMEN'S SINGLE-TRACK SHOE", 'description' => 'Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua consequat.', 'units' => 37, 'price' => 378.00, 'image' => 'http://images.thenorthface.com/is/image/TheNorthFace/236x204_CLR/womens-single-track-shoe-ALQF_JM3_hero.png', 'created_at' => new DateTime, 'updated_at' => null, ], [ 'name' => 'Enduro Boa® Hydration Pack', 'description' => 'Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua consequat.', 'units' => 10, 'price' => 21.10, 'image' => 'http://images.thenorthface.com/is/image/TheNorthFace/236x204_CLR/enduro-boa-hydration-pack-AJQZ_JK3_hero.png', 'created_at' => new DateTime, 'updated_at' => null, ] ]; DB::table('products')->insert($products); } } |
The data we populated the database with is from here and is for sample educational purposes. You may require permissions to actually use the images.
When you are done making the seeder, you need to edit the database/seeds/DatabaseSeeder.php, which actually invokes the seeders:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
//database/seeds/DatabaseSeeder.php use Illuminate\Database\Seeder; class DatabaseSeeder extends Seeder { /** * Seed the application's database. * * @return void */ public function run() { $this->call([ UsersTableSeeder::class, ProductsTableSeeder::class, ]); } } |
Now, when we execute the command to seed our database the DatabaseSeeder class is called and it calls the run which in turn class the seeder classes we had setup to run.
You can learn more about seeders here.
Creating the database for our application
We have defined everything we need our database to have. Now, we need to actually define the database itself. We are going to use SQLite for this guide, but you can any database you like. You should use a non-file based database like MySQL if in a production environment (NOTE: I’m going to use mysql instead).
If you want to use sqlite, Create a database file at database/database.sqlite (NOTE: I’m going to use mysql instead). Next, open your .env file and replace the following lines:
|
1 2 3 |
DB_DATABASE=homestead DB_USERNAME=username DB_PASSWORD=password |
with
|
1 2 3 |
DB_DATABASE=laravel_store DB_USERNAME=your_db_username DB_PASSWORD=your_db_password |
That is all for our database setup. Run the migrations to create the database tables for our application and seed it:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
teddy@teddy:~/Documents/works/laravel/laravel-store$ php artisan migrate --seed Migration table created successfully. Migrating: 2014_10_12_000000_create_users_table Migrated: 2014_10_12_000000_create_users_table Migrating: 2014_10_12_100000_create_password_resets_table Migrated: 2014_10_12_100000_create_password_resets_table Migrating: 2019_01_31_134200_create_products_table Migrated: 2019_01_31_134200_create_products_table Migrating: 2019_01_31_134258_create_orders_table Migrated: 2019_01_31_134258_create_orders_table Seeding: UsersTableSeeder Seeding: ProductsTableSeeder Database seeding completed successfully. |
Defining and securing our endpoints
Our application uses Laravel and Vue to create the best application experience. This means we would need to define APIs to provide our Vue components with data.
Laravel, by default, has support for web and API routes. Web routes handle routing for dynamically generated pages accessed from a web browser, while API routes handle requests from clients that need a response in mostly JSON or XML format.
Our application will have APIs for most requests. We need to secure our APIs to ensure only authorised users will access it. For this, we will use Laravel Passport.
Install passport
To install passport, run the following command:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
teddy@teddy:~/Documents/works/laravel/laravel-store$ composer require laravel/passport Using version ^7.1 for laravel/passport ./composer.json has been updated Loading composer repositories with package information Updating dependencies (including require-dev) Package operations: 7 installs, 0 updates, 0 removals - Installing symfony/psr-http-message-bridge (v1.1.0): Downloading (connectingDownloading (100%) - Installing phpseclib/phpseclib (2.0.14): Downloading (100%) - Installing defuse/php-encryption (v2.2.1): Downloading (100%) - Installing league/event (2.2.0): Downloading (100%) - Installing league/oauth2-server (7.3.2): Downloading (100%) - Installing firebase/php-jwt (v5.0.0): Loading from cache - Installing laravel/passport (v7.1.0): Downloading (100%) symfony/psr-http-message-bridge suggests installing psr/http-factory-implementation (To use the PSR-17 factory) phpseclib/phpseclib suggests installing ext-libsodium (SSH2/SFTP can make use of some algorithms provided by the libsodium-php extension.) phpseclib/phpseclib suggests installing ext-gmp (Install the GMP (GNU Multiple Precision) extension in order to speed up arbitrary precision integer arithmetic operations.) Writing lock file Generating optimized autoload files > Illuminate\Foundation\ComposerScripts::postAutoloadDump > @php artisan package:discover --ansi Discovered Package: beyondcode/laravel-dump-server Discovered Package: fideloper/proxy Discovered Package: laravel/nexmo-notification-channel Discovered Package: laravel/passport Discovered Package: laravel/slack-notification-channel Discovered Package: laravel/tinker Discovered Package: nesbot/carbon Discovered Package: nunomaduro/collision Package manifest generated successfully. |
Laravel Passport comes with it’s own migrations, run the migrations using the following command:
|
1 2 3 4 5 6 7 8 9 10 11 |
teddy@teddy:~/Documents/works/laravel/laravel-store$ php artisan migrate Migrating: 2016_06_01_000001_create_oauth_auth_codes_table Migrated: 2016_06_01_000001_create_oauth_auth_codes_table Migrating: 2016_06_01_000002_create_oauth_access_tokens_table Migrated: 2016_06_01_000002_create_oauth_access_tokens_table Migrating: 2016_06_01_000003_create_oauth_refresh_tokens_table Migrated: 2016_06_01_000003_create_oauth_refresh_tokens_table Migrating: 2016_06_01_000004_create_oauth_clients_table Migrated: 2016_06_01_000004_create_oauth_clients_table Migrating: 2016_06_01_000005_create_oauth_personal_access_clients_table Migrated: 2016_06_01_000005_create_oauth_personal_access_clients_table |
Next, run the passport installation command to create the necessary keys for securing your application:
|
1 2 3 4 5 6 7 8 |
teddy@teddy:~/Documents/works/laravel/laravel-store$ php artisan passport:install Encryption keys generated successfully. Personal access client created successfully. Client ID: 1 Client secret: 5B2jIrNosAkK2OmnqSKyDNshFjdrYMybEXVsbULe Password grant client created successfully. Client ID: 2 Client secret: OTm1dGfx2leOKISA7eRlLsJdx9DtuXidSqyF3CXS |
The above command will create encryption keys needed to generate secure access tokens plus “personal access” and “password grant” clients, which will be used to generate access tokens.
After the installation, you need to use the Laravel Passport HasApiToken trait in the User model. This trait will provide a few helper methods to your model, which allow you to inspect the authenticated user’s token and scopes.
Open the app/User.php file and edit as follows:
|
1 2 3 4 5 6 7 8 9 10 11 12 |
//app/User.php namespace App; ... use Laravel\Passport\HasApiTokens; class User extends Authenticatable { use Notifiable, SoftDeletes, HasApiTokens; ... } |
Next, call the Passport::routes method within the boot method of your AuthServiceProvider. This method will register the routes necessary to issue the tokens your app will need:
In the app/Providers/AuthServiceProvider.php file update as follows:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
//app/Providers/AuthServiceProvider.php namespace App\Providers; ... use Laravel\Passport\Passport; class AuthServiceProvider extends ServiceProvider { ... /** * Register any authentication / authorization services. * * @return void */ public function boot() { $this->registerPolicies(); Passport::routes(); } } |
Finally, in your config/auth.php configuration file, you should set the driver option of the api authentication guard to passport.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
//config/auth.php [...] 'guards' => [ [...] 'api' => [ 'driver' => 'passport', 'provider' => 'users', ], ], [...] |
That’s all for Laravel Passport.
Conclusion
So far, we have set up our application by defining all the models we need. We have also added migrations for our database, and lastly, we added Laravel Passport for handling authorization for our APIs.
The next thing we will look at is implementing the controllers to handle the different kinds of request we are going to make to the application and the views to interact with the application. Continue to part two.
In the previous chapter, we set up our application’s migrations and models, and installed Laravel Passport for authentication. We also planned what the application will look like. In this chapter, we will implement the controllers and handle all requests to our application.
Prerequisites
To continue with this part, please go through the first part of the series first and make sure you have all the requirements from that part.
Building our controllers
In the first chapter, we already defined our models and their accompanying controllers. These controllers reside in the app/Http/Controllers directory. The User model, however, does not have an accompanying controller, so we are going to create that first. Run the following command:
|
1 2 |
teddy@teddy:~/Documents/works/laravel/laravel-store$ php artisan make:controller UserController Controller created successfully. |
Now, open the created controller file app/Http/Controllers/UserController.php and replace the contents with the following:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 |
//app/Http/Controllers/UserController.php namespace App\Http\Controllers; use Illuminate\Http\Request; use App\User; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Validator; class UserController extends Controller { public function index() { return response()->json(User::with(['orders'])->get()); } public function login(Request $request) { $status = 401; $response = ['error' => 'Unauthorized']; if (Auth::attempt($request->only(['email', 'password']))) { $status = 200; $response = [ 'user' => Auth::user(), 'token' => Auth::user()->createToken('myStore')->accessToken, ]; } return response()->json($response, $status); } public function register(Request $request) { $validator = Validator::make($request->all(), [ 'name' => 'required|max:50', 'email' => 'required|email', 'password' => 'required|min:6', 'c_password' => 'required|same:password', ]); if ($validator->fails()) { return response()->json(['error' => $validator->errors()], 401); } $data = $request->only(['name', 'email', 'password']); $data['password'] = bcrypt($data['password']); $user = User::create($data); $user->is_admin = 0; return response()->json([ 'user' => $user, 'token' => $user->createToken('myStore')->accessToken, ]); } public function show(User $user) { return response()->json($user); } public function showOrders(User $user) { return response()->json($user->orders()->with(['product'])->get()); } } |
Above we defined some class methods:
index() – returns all users with their orders.
login() – authenticates a user and generates an access token for that user. The createToken method is one of the methods Laravel Passport adds to our user model.
register() – creates a user account, authenticates it and generates an access token for it.
show() – gets the details of a user and returns them.
showOrders() – gets all the orders of a user and returns them.
We used Laravel’s Route-Model Binding to automatically inject our model instance into the controller. The only caveat is that the variable name used for the binding has to be the same as the one defined in the route as well.
Next, open the app/Http/Controllers/ProductController.php file and replace the contents with the following:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 |
//app/Http/Controllers/ProductController.php namespace App\Http\Controllers; use App\Product; use Illuminate\Http\Request; class ProductController extends Controller { public function index() { return response()->json(Product::all(),200); } public function store(Request $request) { $product = Product::create([ 'name' => $request->name, 'description' => $request->description, 'units' => $request->units, 'price' => $request->price, 'image' => $request->image ]); return response()->json([ 'status' => (bool) $product, 'data' => $product, 'message' => $product ? 'Product Created!' : 'Error Creating Product' ]); } public function show(Product $product) { return response()->json($product,200); } public function uploadFile(Request $request) { if($request->hasFile('image')){ $name = time()."_".$request->file('image')->getClientOriginalName(); $request->file('image')->move(public_path('images'), $name); } return response()->json(asset("images/$name"),201); } public function update(Request $request, Product $product) { $status = $product->update( $request->only(['name', 'description', 'units', 'price', 'image']) ); return response()->json([ 'status' => $status, 'message' => $status ? 'Product Updated!' : 'Error Updating Product' ]); } public function updateUnits(Request $request, Product $product) { $product->units = $product->units + $request->get('units'); $status = $product->save(); return response()->json([ 'status' => $status, 'message' => $status ? 'Units Added!' : 'Error Adding Product Units' ]); } public function destroy(Product $product) { $status = $product->delete(); return response()->json([ 'status' => $status, 'message' => $status ? 'Product Deleted!' : 'Error Deleting Product' ]); } } |
In the ProductController above we defined seven methods:
index() – fetches and returns all the product records.
store() – creates a product record.
show() – fetches and returns a single product.
uploadFile() – uploads the image for a product we created and returns the url for the product.
update() – updates the product record.
updateUnits() – adds new units to a product.
delete() – deletes a product.
Next, open the app/Http/Controllers/OrderController.php file and replace the content with the following:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 |
//app/Http/Controllers/OrderController.php namespace App\Http\Controllers; use App\Order; use Illuminate\Support\Facades\Auth; use Illuminate\Http\Request; class OrderController extends Controller { public function index() { return response()->json(Order::with(['product'])->get(),200); } public function deliverOrder(Order $order) { $order->is_delivered = true; $status = $order->save(); return response()->json([ 'status' => $status, 'data' => $order, 'message' => $status ? 'Order Delivered!' : 'Error Delivering Order' ]); } public function store(Request $request) { $order = Order::create([ 'product_id' => $request->product_id, 'user_id' => Auth::id(), 'quantity' => $request->quantity, 'address' => $request->address ]); return response()->json([ 'status' => (bool) $order, 'data' => $order, 'message' => $order ? 'Order Created!' : 'Error Creating Order' ]); } public function show(Order $order) { return response()->json($order,200); } public function update(Request $request, Order $order) { $status = $order->update( $request->only(['quantity']) ); return response()->json([ 'status' => $status, 'message' => $status ? 'Order Updated!' : 'Error Updating Order' ]); } public function destroy(Order $order) { $status = $order->delete(); return response()->json([ 'status' => $status, 'message' => $status ? 'Order Deleted!' : 'Error Deleting Order' ]); } } |
In the OrderController above we have six methods:
index() – fetches and returns all the orders.
deliverOrder() – marks an order as delivered.
store() – creates an order.
show() – fetches and returns a single order.
update() – updates the order.
destroy() – deletes an order.
That’s it for our controllers. We have created the controller according to the specifications we laid out in the first part. Next thing we need to do is define our API routes.
Defining our application’s routes
Now that we have fully defined all the requests we would like to make to our application, let’s expose the APIs for making these requests. Open routes/api.php file and replace the content with the following:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
//routes/api.php use Illuminate\Http\Request; Route::post('login', 'UserController@login'); Route::post('register', 'UserController@register'); Route::get('/products', 'ProductController@index'); Route::post('/upload-file', 'ProductController@uploadFile'); Route::get('/products/{product}', 'ProductController@show'); Route::group(['middleware' => 'auth:api'], function(){ Route::get('/users','UserController@index'); Route::get('users/{user}','UserController@show'); Route::patch('users/{user}','UserController@update'); Route::get('users/{user}/orders','UserController@showOrders'); Route::patch('products/{product}/units/add','ProductController@updateUnits'); Route::patch('orders/{order}/deliver','OrderController@deliverOrder'); Route::resource('/orders', 'OrderController'); Route::resource('/products', 'ProductController')->except(['index','show']); }); |
Putting our route definitions in the routes/api.php file will tell Laravel they are API routes so Laravel will prefix the routes with a /api in the URL to differentiate them from web-routes.
Adding the auth:api middleware ensures any calls to the routes in that group must be authenticated.
A thing to note is, using the resource method on the Route class helps us create some additional routes under the hood without us having to create them manually. Read about resource controllers and routes here.
To see the full route list, run the following command: $ php artisan route:list
Since we will build the front end of this application in Vue, we need to define the web routes for it. Open the routes/web.php file and replace the contents with the following:
|
1 2 3 4 5 |
//routes/web.php Route::get('/{any}', function(){ return view('landing'); })->where('any', '.*'); |
This will route every web request to a single entry point, which will be the entry for your Vue application.
Setting up Vue for the frontend
Vue is a progressive framework for building user interfaces. Unlike other monolithic frameworks, Vue is designed from the ground up to be incrementally adoptable – vuejs.org
Laravel comes with Vue bundled out of the box, so all we need to do to get Vue is to install the node packages. Run the following command:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
teddy@teddy:~/Documents/works/laravel/laravel-store$ npm install > webpack-cli@3.2.1 postinstall /home/teddy/Documents/works/laravel/laravel-store/node_modules/webpack-cli > lightercollective *** Thank you for using webpack-cli! *** Please consider donating to our open collective to help us maintain this package. https://opencollective.com/webpack/donate *** npm notice created a lockfile as package-lock.json. You should commit this file. npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.7 (node_modules/fsevents): npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.7: wanted {"os":"darwin","arch":"any"} (current: {"os":"linux","arch":"x64"}) added 943 packages from 442 contributors and audited 16939 packages in 14.858s found 0 vulnerabilities |
Next, we will need VueRouter to handle the routing between the different components of our Vue application. To install VueRouter run the command below:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
teddy@teddy:~/Documents/works/laravel/laravel-store$ npm i vue-router npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.7 (node_modules/fsevents): npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.7: wanted {"os":"darwin","arch":"any"} (current: {"os":"linux","arch":"x64"}) + vue-router@3.0.2 added 1 package from 1 contributor and audited 16940 packages in 5.414s found 0 vulnerabilities ╭───────────────────────────────────────────────────────────────╮ │ │ │ New minor version of npm available! 6.5.0 → 6.7.0 │ │ Changelog: https://github.com/npm/cli/releases/tag/v6.7.0 │ │ Run npm install -g npm to update! │ │ │ ╰───────────────────────────────────────────────────────────────╯ |
|
1 2 3 4 5 6 7 |
teddy@teddy:~/Documents/works/laravel/laravel-store$ npm i -g npm /home/teddy/.nvm/versions/node/v8.9.4/bin/npm -> /home/teddy/.nvm/versions/node/v8.9.4/lib/node_modules/npm/bin/npm-cli.js /home/teddy/.nvm/versions/node/v8.9.4/bin/npx -> /home/teddy/.nvm/versions/node/v8.9.4/lib/node_modules/npm/bin/npx-cli.js + npm@6.7.0 added 50 packages from 7 contributors, removed 13 packages and updated 31 packages in 5.253s teddy@teddy:~/Documents/works/laravel/laravel-store$ npm --v 6.7.0 |
Next, let’s make the landing view file, which would mount our Vue application. Create the file resources/views/landing.blade.php and add the following code:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
<!-- resources/views/landing.blade.php --> <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="csrf-token" content="{{csrf_token()}}"> <title>My Store</title> <link href=" {{ mix('css/app.css') }}" rel="stylesheet"> </head> <body> <div id="app"> <app></app> </div> <script src="{{ mix('js/bootstrap.js') }}"></script> <script src="{{ mix('js/app.js') }}"></script> </body> </html> |
In the code above, we have the HTML for our application. If you look closely, you can see the app tag. This will be the entry point to our Vue application and where the components will be loaded.
Since we will use app.js to set up our VueRouter, we still need to have Bootstrap and Axios compiled. The import for Bootstrap and Axios is in the bootstrap.js file so we need to compile that.
Edit the webpack.mix.js file so it compiles all assets:
|
1 2 3 4 5 6 |
<!-- webpack.mix.js --> [...] mix.js('resources/assets/js/app.js', 'public/js') .js('resources/assets/js/bootstrap.js', 'public/js') .sass('resources/assets/sass/app.scss', 'public/css'); |
the webpack.mix.js file holds the configuration files for laravel-mix, which provides a wrapper around Webpack. It lets us take advantage of Webpack’s amazing asset compilation abilities without having to write Webpack configurations by ourselves. You can learn more about Webpack here.
Set up the homepage for the Vue application. Create a new file, resources/js/components/Home.vue, and add the following code to the file:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
<!-- resources/js/components/Home.vue --> <template> <div> <div class="container-fluid hero-section d-flex align-content-center justify-content-center flex-wrap ml-auto"> <h2 class="title">Welcome to My Store</h2> </div> <div class="container"> <div class="row"> <div class="col-md-12"> <div class="row"> <div class="col-md-4 product-box" v-for="(product,index) in products" @key="index"> <router-link :to="{ path: '/products/'+product.id}"> <img :src="product.image" :alt="product.name"> <h5><span v-html="product.name"></span> <span class="small-text text-muted float-right">$ {{product.price}}</span> </h5> <button class="col-md-4 btn btn-sm btn-primary float-right">Buy Now</button> </router-link> </div> </div> </div> </div> </div> </div> </template> <script> export default { data() { return { products : [] } }, mounted() { axios.get("api/products").then(response => { this.products = response.data; }); } } </script> |
The code above within the opening and closing template tag we have the HTML of our Vue component. In there we loop through the contents of products and for each product we display the image, name, id, price and units available. We use the v-html attribute to render raw HTML, which makes it easy for us to use special characters in the product name.
In the script tag, we defined the data(), which holds all the variables we can use in our template. We also defined the mounted() method, which is called after our component is loaded. In this mounted method, we load our products from the API then set the products variable so that our template would be updated with API data.
In the same file, append the code below to the bottom:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
<style scoped> .small-text { font-size: 14px; } .product-box { border: 1px solid #cccccc; padding: 10px 15px; } .hero-section { height: 30vh; background: #ababab; align-items: center; margin-bottom: 20px; margin-top: -20px; } .title { font-size: 60px; color: #ffffff; } </style> |
In the code above, we have defined the style to use with the welcome component.
According to the Vue documentation: When a <style> tag has the scoped attribute, its CSS will apply to elements of the current component only. This is similar to the style encapsulation found in the Shadow DOM. It comes with some caveats but doesn’t require any polyfills.
Next create another file, resources/js/components/App.vue. This will be the application container where all other components will be loaded. In this file, add the following code:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
<!-- resources/js/components/App.vue --> <template> <div> <nav class="navbar navbar-expand-md navbar-light navbar-laravel"> <div class="container"> <router-link :to="{name: 'home'}" class="navbar-brand">My Store</router-link> <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation"> <span class="navbar-toggler-icon"></span> </button> <div class="collapse navbar-collapse" id="navbarSupportedContent"> <!-- Left Side Of Navbar --> <ul class="navbar-nav mr-auto"></ul> <!-- Right Side Of Navbar --> <ul class="navbar-nav ml-auto"> <router-link :to="{ name: 'login' }" class="nav-link" v-if="!isLoggedIn">Login</router-link> <router-link :to="{ name: 'register' }" class="nav-link" v-if="!isLoggedIn">Register</router-link> <span v-if="isLoggedIn"> <router-link :to="{ name: 'userboard' }" class="nav-link" v-if="user_type == 0"> Hi, {{name}}</router-link> <router-link :to="{ name: 'admin' }" class="nav-link" v-if="user_type == 1"> Hi, {{name}}</router-link> </span> <li class="nav-link" v-if="isLoggedIn" @click="logout"> Logout</li> </ul> </div> </div> </nav> <main class="py-4"> <router-view @loggedIn="change"></router-view> </main> </div> </template> |
In the Vue template above we used some Vue specific tags like router-link, which helps us generate links for routing to pages defined in our router. We also have the router-view, which is where all the child component pages will be loaded.
Next, below the closing template tag, add the following code:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
<script> export default { data() { return { name: null, user_type: 0, isLoggedIn: localStorage.getItem('myStore.jwt') != null } }, mounted() { this.setDefaults(); }, methods : { setDefaults() { if(this.isLoggedIn){ let user = JSON.parse(localStorage.getItem('myStore.user')); this.name = user.name; this.user_type = user.is_admin; } }, change() { this.isLoggedIn = localStorage.getItem('myStore.jwt') != null; this.setDefaults(); }, logout() { localStorage.removeItem('myStore.jwt'); localStorage.removeItem('myStore.user'); this.change(); this.$router.push('/'); } } } </script> |
In the script definition we have the methods property and in there we have three methods defined:
setDefaults() – sets the name of the user when the user is logged in as well as the type of user logged in.
change()– checks the current login status anytime it is called and calls the setDefaults method.
logout() – logs the user out of the application and routes the user to the homepage.
In our router-view component, we listen for an event loggedIn which calls the change method. This event is fired by our component anytime we log in. It is a way of telling the App component to update itself when a user logs in.
Next create the following files in the resources/js/components directory:
Admin.vue
Checkout.vue
Confirmation.vue
Login.vue
Register.vue
SingleProduct.vue
UserBoard.vue
These files would hold all the pages myStore would have. They need to be created prior to setting up VueRouter, so that it won’t throw an error.
To set up the routing for our Vue single page app, open your resources/js/app.js file and replace the contents with the following code:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 |
//resources/js/app.js ... import Vue from 'vue' import VueRouter from 'vue-router' Vue.use(VueRouter) import App from './components/App' import Home from './components/Home' import Login from './components/Login' import Register from './components/Register' import SingleProduct from './components/SingleProduct' import Checkout from './components/Checkout' import Confirmation from './components/Confirmation' import UserBoard from './components/UserBoard' import Admin from './components/Admin' const router = new VueRouter({ mode: 'history', routes: [ { path: '/', name: 'home', component: Home }, { path: '/login', name: 'login', component: Login }, { path: '/register', name: 'register', component: Register }, { path: '/products/:id', name: 'single-products', component: SingleProduct }, { path: '/confirmation', name: 'confirmation', component: Confirmation }, { path: '/checkout', name: 'checkout', component: Checkout, props: (route) => ({ pid: route.query.pid }) }, { path: '/dashboard', name: 'userboard', component: UserBoard, meta: { requiresAuth: true, is_user: true } }, { path: '/admin/:page', name: 'admin-pages', component: Admin, meta: { requiresAuth: true, is_admin: true } }, { path: '/admin', name: 'admin', component: Admin, meta: { requiresAuth: true, is_admin: true } }, ], }) router.beforeEach((to, from, next) => { if (to.matched.some(record => record.meta.requiresAuth)) { if (localStorage.getItem('myStore.jwt') == null) { next({ path: '/login', params: { nextUrl: to.fullPath } }) } else { let user = JSON.parse(localStorage.getItem('myStore.user')) if (to.matched.some(record => record.meta.is_admin)) { if (user.is_admin == 1) { next() } else { next({ name: 'userboard' }) } } else if (to.matched.some(record => record.meta.is_user)) { if (user.is_admin == 0) { next() } else { next({ name: 'admin' }) } } next() } } else { next() } }) ... |
Above, we have imported the VueRouter and we added it to our Vue application. We defined routes for our application and then registered it to the Vue instance so it is available to all Vue components.
Each of the route objects has a name, which we will use to identify and invoke that route. It also has a path, which you can visit directly in your browser. Lastly, it has a component, which is mounted when you visit the route.
On some routes, we defined meta, which contains variables we would like to check when we access the route. In our case, we are checking if the route requires authentication and if it is restricted to administrators or regular users only.
We set up the beforeEach middleware on the router that checks each route before going to it. The method takes these variables:
to – the route you want to move to.
from – the current route you are moving away from.
next – the method that finally moves to a defined route. When called without a route passed, it continues the navigation. If given a route, it goes to that route.
We use beforeEach to check the routes that require authentication before you can access them. For those routes, we check if the user is authenticated. If the user isn’t, we send them to the login page. If the user is authenticated, we check if the route is restricted to admin users or regular users. We redirect each user to the right place based on which access level they have.
Now add the following lines to the end of the app.js file
|
1 2 3 4 5 |
const app = new Vue({ el: '#app', components: { App }, router, }); |
This instantiates the Vue application. In this global instance, we mount the App component only because the VueRouter needs it to switch between all the other components.
Now, we are ready to start making the other views for our application.
Conclusion
In this part, we implemented the controller logic that handles all the requests to our application and defined all the routes the application will use. We also set up Vue and VueRouter to prepare our application for building the core frontend.
In the next chapter of this guide, we are going to build the core frontend of the application and consume the APIs. See you in the next part.
Making the authentication pages
In the previous part, we created all the view files for our Vue application, although we did not add content to all of them. Let’s start with the login and register pages.
Open the resources/js/components/Login.vue file and paste in the following code:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
<!-- resources/js/components/Login.vue --> <template> <div class="container"> <div class="row justify-content-center"> <div class="col-md-8"> <div class="card card-default"> <div class="card-header">Login</div> <div class="card-body"> <form> <div class="form-group row"> <label for="email" class="col-sm-4 col-form-label text-md-right">E-Mail Address</label> <div class="col-md-6"> <input id="email" type="email" class="form-control" v-model="email" required autofocus> </div> </div> <div class="form-group row"> <label for="password" class="col-md-4 col-form-label text-md-right">Password</label> <div class="col-md-6"> <input id="password" type="password" class="form-control" v-model="password" required> </div> </div> <div class="form-group row mb-0"> <div class="col-md-8 offset-md-4"> <button type="submit" class="btn btn-primary" @click="handleSubmit"> Login </button> </div> </div> </form> </div> </div> </div> </div> </div> </template> |
Above we have a login form. We don’t have much functionality yet so let’s append the following code to the same file to add some Vue scripting:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
<script> export default { data() { return { email: "", password: "" } }, methods: { handleSubmit(e) { e.preventDefault() if (this.password.length > 0) { let email = this.email let password = this.password axios.post('api/login', {email, password}).then(response => { let user = response.data.user let is_admin = user.is_admin localStorage.setItem('myStore.user', JSON.stringify(user)) localStorage.setItem('myStore.jwt', response.data.token) if (localStorage.getItem('myStore.jwt') != null) { this.$emit('loggedIn') if (this.$route.params.nextUrl != null) { this.$router.push(this.$route.params.nextUrl) } else { this.$router.push((is_admin == 1 ? 'admin' : 'dashboard')) } } }); } } } } </script> |
Above we have a handleSubmit method that is fired when the form is submitted. In this method we attempt to authenticate using the API. If the authentication is successful, we save the access token and user data in localStorage so we can access them across our app.
We also emit a loggedIn event so the parent component can update as well. Lastly, we check if the user was sent to the login page from another page, then send the user to that page. If the user came to login directly, we check the user type and redirect the user appropriately.
Next, open the resources/js/components/Register.vue file and paste in the following:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
<template> <div class="container"> <div class="row justify-content-center"> <div class="col-md-8"> <div class="card card-default"> <div class="card-header">Register</div> <div class="card-body"> <form> <div class="form-group row"> <label for="name" class="col-md-4 col-form-label text-md-right">Name</label> <div class="col-md-6"> <input id="name" type="text" class="form-control" v-model="name" required autofocus> </div> </div> <div class="form-group row"> <label for="email" class="col-md-4 col-form-label text-md-right">E-Mail Address</label> <div class="col-md-6"> <input id="email" type="email" class="form-control" v-model="email" required> </div> </div> <div class="form-group row"> <label for="password" class="col-md-4 col-form-label text-md-right">Password</label> <div class="col-md-6"> <input id="password" type="password" class="form-control" v-model="password" required> </div> </div> <div class="form-group row"> <label for="password-confirm" class="col-md-4 col-form-label text-md-right">Confirm Password</label> <div class="col-md-6"> <input id="password-confirm" type="password" class="form-control" v-model="password_confirmation" required> </div> </div> <div class="form-group row mb-0"> <div class="col-md-6 offset-md-4"> <button type="submit" class="btn btn-primary" @click="handleSubmit"> Register </button> </div> </div> </form> </div> </div> </div> </div> </div> </template> |
Above we have the HTML for the registration form. Let’s add the script for the component below the closing template tag:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
<script> export default { props : ['nextUrl'], data(){ return { name : "", email : "", password : "", password_confirmation : "" } }, methods : { handleSubmit(e) { e.preventDefault() if (this.password.length <= 5) { this.password = "" this.password_confirmation = "" return alert('Passwords must contain 6 or more characters') } if (this.password !== this.password_confirmation) { this.password = "" this.password_confirmation = "" return alert('Passwords do not match') } let name = this.name let email = this.email let password = this.password let c_password = this.password_confirmation axios.post('api/register', {name, email, password, c_password}).then(response => { let data = response.data localStorage.setItem('myStore.user', JSON.stringify(data.user)) localStorage.setItem('myStore.jwt', data.token) if (localStorage.getItem('myStore.jwt') != null) { this.$emit('loggedIn') let nextUrl = this.$route.params.nextUrl this.$router.push((nextUrl != null ? nextUrl : '/')) } }) } } } </script> |
NOTE: I added a check for the password and the confirmation characters length. It should be more than 5 characters.
The register component operates similarly to the login component. We send the user data to the API for authentication and if we get a favourable response we save the token and user to localStorage.
Making the marketplace pages
We had already defined the homepage in the last chapter and returned a list of available products. Now, we are going to make all the other store pages.
Open the resources/js/components/SingleProduct.vue file and paste in the following code:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
<template> <div class="container"> <div class="row"> <div class="col-md-8 offset-md-2"> <img :src="product.image" :alt="product.name"> <h3 class="title" v-html="product.name"></h3> <p class="text-muted">{{product.description}}</p> <h4> <span class="small-text text-muted float-left">$ {{product.price}}</span> <span class="small-text float-right">Available Quantity: {{product.units}}</span> </h4> <br> <hr> <router-link :to="{ path: '/checkout?pid='+product.id }" class="col-md-4 btn btn-sm btn-primary float-right">Buy Now</router-link> </div> </div> </div> </template> <script> export default { data(){ return { product : [] } }, beforeMount(){ let url = `/api/products/${this.$route.params.id}` axios.get(url).then(response => this.product = response.data) } } </script> <style scoped> .small-text { font-size: 18px; } .title { font-size: 36px; } </style> |
NOTE: USE backtick (`) INSTEAD OF apostroph (‘) ON let url VARIABLE TO PARSE ${this.$route.params.id}
Above we have the product as a data attribute, which we use to display information on the page like we did in the Home.vue file.
In the components script we defined Vue’s beforeMount method and fetched the product information there. beforeMount is called before the component is rendered, so it fetches the necessary data for rendering the component. If we get the data after the component has mounted, we would have errors before the component updates.
Next, open the resources/js/components/Checkout.vue file and paste the following code for the HTML template and style:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 |
<template> <div class="container"> <div class="row"> <div class="col-md-8 offset-md-2"> <div class="order-box"> <img :src="product.image" :alt="product.name"> <h2 class="title" v-html="product.name"></h2> <p class="small-text text-muted float-left">$ {{product.price}}</p> <p class="small-text text-muted float-right">Available Units: {{product.units}}</p> <br> <hr> <label class="row"><span class="col-md-2 float-left">Quantity: </span><input type="number" name="units" min="1" :max="product.units" class="col-md-2 float-left" v-model="quantity" @change="checkUnits"></label> </div> <br> <div> <div v-if="!isLoggedIn"> <h2>You need to login to continue</h2> <button class="col-md-4 btn btn-primary float-left" @click="login">Login</button> <button class="col-md-4 btn btn-danger float-right" @click="register">Create an account</button> </div> <div v-if="isLoggedIn"> <div class="row"> <label for="address" class="col-md-3 col-form-label">Delivery Address</label> <div class="col-md-9"> <input id="address" type="text" class="form-control" v-model="address" required> </div> </div> <br> <button class="col-md-4 btn btn-sm btn-success float-right" v-if="isLoggedIn" @click="placeOrder">Continue</button> </div> </div> </div> </div> </div> </template> <script> export default { props : ['pid'], data(){ return { address : "", quantity : 1, isLoggedIn : null, product : [] } }, mounted() { this.isLoggedIn = localStorage.getItem('myStore.jwt') != null }, beforeMount() { axios.get(`/api/products/${this.pid}`).then(response => this.product = response.data) if (localStorage.getItem('myStore.jwt') != null) { this.user = JSON.parse(localStorage.getItem('myStore.user')) axios.defaults.headers.common['Content-Type'] = 'application/json' axios.defaults.headers.common['Authorization'] = 'Bearer ' + localStorage.getItem('myStore.jwt') } }, methods : { login() { this.$router.push({name: 'login', params: {nextUrl: this.$route.fullPath}}) }, register() { this.$router.push({name: 'register', params: {nextUrl: this.$route.fullPath}}) }, placeOrder(e) { e.preventDefault() let address = this.address let product_id = this.product.id let quantity = this.quantity axios.post('api/orders/', {address, quantity, product_id}) .then(response => this.$router.push('/confirmation')) }, checkUnits(e){ if (this.quantity > this.product.units) { this.quantity = this.product.units } } } } </script> <style scoped> .small-text { font-size: 18px; } .order-box { border: 1px solid #cccccc; padding: 10px 15px; } .title { font-size: 36px; } </style> |
Above we defined the beforeMount method where we fetch product information. Then we have the mounted method where we check authentication status. In the methods property, we defined the checkUnits method that checks how many units the user wants to order, and then we define the placeOrder method that places the order.
Next, open the resources/js/components/Confirmation.vue file and paste the following code:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
<template> <div> <div class="container-fluid hero-section d-flex align-content-center justify-content-center flex-wrap ml-auto"> <h2> <span class="title"><strong>Thank You!</strong></span><br> <span class="medium-text">Your order has been placed.</span><br> <router-link :to="{name: 'userboard'}" class="small-link"> See your orders </router-link> </h2> </div> </div> </template> <script> export default {} </script> <style scoped> .medium-text { font-size: 36px; } .small-link { font-size: 24px; text-decoration: underline; color: #777; } .product-box { border: 1px solid #cccccc; padding: 10px 15px; } .hero-section { height: 80vh; align-items: center; margin-top: -20px; margin-bottom: 20px; } .title { font-size: 60px; } </style> |
This component displays a simple thank you message and provides a link for the users to navigate to their dashboard to see the orders they have placed and the status of these orders. Let’s create the user dashboard next.
Creating the user dashboard
The user dashboard is where the users can see all their orders. Open the resources/js/components/UserBoard.vue file and paste in the following code for the template and style:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
<template> <div> <div class="container-fluid hero-section d-flex align-content-center justify-content-center flex-wrap ml-auto"> <h2 class="title">All your orders</h2> </div> <div class="container"> <div class="row"> <div class="col-md-12"> <br> <div class="row"> <div class="col-md-4 product-box" v-for="(order,index) in orders" @key="index"> <img :src="order.product.image" :alt="order.product.name"> <h5><span v-html="order.product.name"></span><br> <span class="small-text text-muted">$ {{order.product.price}}</span> </h5> <hr> <span class="small-text text-muted">Quantity: {{order.quantity}} <span class="float-right">{{order.is_delivered == 1? "shipped!" : "not shipped"}}</span> </span> <br><br> <p><strong>Delivery address:</strong> <br>{{order.address}}</p> </div> </div> </div> </div> </div> </div> </template> <script> export default { data() { return { user : null, orders : [] } }, beforeMount() { this.user = JSON.parse(localStorage.getItem('myStore.user')) axios.defaults.headers.common['Content-Type'] = 'application/json' axios.defaults.headers.common['Authorization'] = 'Bearer ' + localStorage.getItem('myStore.jwt') axios.get(`api/users/${this.user.id}/orders`) .then(response => this.orders = response.data) } } </script> <style scoped> .small-text { font-size: 14px; } .product-box { border: 1px solid #cccccc; padding: 10px 15px; } .hero-section { background: #ababab; height: 20vh; align-items: center; margin-bottom: 20px; margin-top: -20px; } .title { font-size: 60px; color: #ffffff; } </style> |
In the code above, we fetch all the user’s orders before the component is mounted, then loop through them and display them on the page.
Creating the admin dashboard
The admin dashboard is where new products are added, existing products modified and orders are set as delivered.
For the admin board, we will use four different components, which we will mount based on the url a user is accessing. Let’s see that in action. Open the resources/js/components/Admin.vue file and paste in the following:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 |
<template> <div> <div class="container-fluid hero-section d-flex align-content-center justify-content-center flex-wrap ml-auto"> <h2 class="title">Admin Dashboard</h2> </div> <div class="container"> <div class="row"> <div class="col-md-3"> <ul style="list-style-type:none"> <li class="active"><button class="btn" @click="setComponent('main')">Dashboard</button></li> <li><button class="btn" @click="setComponent('orders')">Orders</button></li> <li><button class="btn" @click="setComponent('products')">Products</button></li> <li><button class="btn" @click="setComponent('users')">Users</button></li> </ul> </div> <div class="col-md-9"> <component :is="activeComponent"></component> </div> </div> </div> </div> </template> <script> import Main from '../components/admin/Main' import Users from '../components/admin/Users' import Products from '../components/admin/Products' import Orders from '../components/admin/Orders' export default { data() { return { user: null, activeComponent: null } }, components: { Main, Users, Products, Orders }, beforeMount() { this.setComponent(this.$route.params.page) this.user = JSON.parse(localStorage.getItem('myStore.user')) axios.defaults.headers.common['Content-Type'] = 'application/json' axios.defaults.headers.common['Authorization'] = 'Bearer ' + localStorage.getItem('myStore.jwt') }, methods: { setComponent(value) { switch(value) { case "users": this.activeComponent = Users this.$router.push({name: 'admin-pages', params: {page: 'users'}}) break; case "orders": this.activeComponent = Orders this.$router.push({name: 'admin-pages', params: {page: 'orders'}}) break; case "products": this.activeComponent = Products this.$router.push({name: 'admin-pages', params: {page: 'products'}}) break; default: this.activeComponent = Main this.$router.push({name: 'admin'}) break; } } } } </script> <style scoped> .hero-section { height: 20vh; background: #ababab; align-items: center; margin-bottom: 20px; margin-top: -20px; } .title { font-size: 60px; color: #ffffff; } </style> |
In the code above, we import and register four components, which we have not yet created. They’ll be used as components inside Admin.vue parent component.
In our template, we defined the navigation for switching between the components. Each navigation link calls the setComponent method and then passes a value to it. The setComponent method just sets the component using a switch statement.
Let’s create the first component for the Admin component. Create the Main.vue file in resources/js/components/admin directory and paste the following into the file:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
<template> <div class="row"> <div class="col-md-4 product-box d-flex align-content-center justify-content-center flex-wrap big-text"> <a href='/admin/orders'>Orders ({{orders.length}})</a> </div> <hr> <div class="col-md-4 product-box d-flex align-content-center justify-content-center flex-wrap big-text"> <a href='/admin/products'>Products ({{products.length}})</a> </div> <div class="col-md-4 product-box d-flex align-content-center justify-content-center flex-wrap big-text"> <a href='/admin/users'>Users ({{users.length}})</a> </div> </div> </template> <script> export default { data() { return { user : null, orders : [], products : [], users : [] } }, mounted() { axios.get('/api/users/').then(response => this.users = response.data) axios.get('/api/products/').then(response => this.products = response.data) axios.get('/api/orders/').then(response => this.orders = response.data) } } </script> <style scoped> .big-text { font-size: 28px; } .product-box { border: 1px solid #cccccc; padding: 10px 15px; height: 20vh } </style> |
In the code above we are calling the APIs for users, orders and products, and returning their data.
Create the Orders.vue file in resources/js/components/admin and paste the following into the file:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
<template> <div> <table class="table table-responsive table-striped"> <thead> <tr> <td></td> <td>Product</td> <td>Quantity</td> <td>Cost</td> <td>Delivery Address</td> <td>is Delivered?</td> <td>Action</td> </tr> </thead> <tbody> <tr v-for="(order,index) in orders" @key="index"> <td>{{index+1}}</td> <td v-html="order.product.name"></td> <td>{{order.quantity}}</td> <td>{{order.quantity * order.product.price}}</td> <td>{{order.address}}</td> <td>{{order.is_delivered == 1? "Yes" : "No"}}</td> <td v-if="order.is_delivered == 0"><button class="btn btn-success" @click="deliver(index)">Deliver</button></td> </tr> </tbody> </table> </div> </template> <script> export default { data() { return { orders : [] } }, beforeMount(){ axios.get('/api/orders/').then(response => this.orders = response.data) }, methods: { deliver(index) { let order = this.orders[index] axios.patch(`/api/orders/${order.id}/deliver`).then(response => { this.orders[index].is_delivered = 1 this.$forceUpdate() }) } } } </script> |
In beforeMount we fetch all the orders placed before the component is rendered.
When the Deliver button is clicked, the deliver method is fired. We call the API for delivering orders. To get the change to reflect on the page instantly, we call [$forceUpdate](https://vuejs.org/v2/api/#vm-forceUpdate).
Create the Users.vue file in resources/js/components/admin and paste in the following code:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
<template> <div> <table class="table table-responsive table-striped"> <thead> <tr> <td></td> <td>Name</td> <td>Email</td> <td>Joined</td> <td>Total Orders</td> </tr> </thead> <tbody> <tr v-for="(user,index) in users" @key="index"> <td>{{index+1}}</td> <td>{{user.name}}</td> <td>{{user.email}}</td> <td>{{user.created_at}}</td> <td>{{user.orders.length}}</td> </tr> </tbody> </table> </div> </template> <script> export default { data() { return { users : [] } }, beforeMount() { axios.get('/api/users/').then(response => this.users = response.data) } } </script> |
Above we fetch all the user data and then display it on the page.
Next, create the Products.vue file in resources/js/components/admin and paste the following template code:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 |
<template> <div> <table class="table table-responsive table-striped"> <thead> <tr> <td></td> <td>Product</td> <td>Units</td> <td>Price</td> <td>Description</td> </tr> </thead> <tbody> <tr v-for="(product,index) in products" @key="index" @dblclick="editingItem = product"> <td>{{index+1}}</td> <td v-html="product.name"></td> <td v-model="product.units">{{product.units}}</td> <td v-model="product.price">{{product.price}}</td> <td v-model="product.price">{{product.description}}</td> </tr> </tbody> </table> <modal @close="endEditing" :product="editingItem" v-show="editingItem != null"></modal> <modal @close="addProduct" :product="addingProduct" v-show="addingProduct != null"></modal> <br> <button class="btn btn-primary" @click="newProduct">Add New Product</button> </div> </template> <script> import Modal from './ProductModal' export default { data() { return { products: [], editingItem: null, addingProduct: null } }, components: {Modal}, beforeMount() { axios.get('/api/products/').then(response => this.products = response.data) }, methods: { newProduct() { this.addingProduct = { name: null, units: null, price: null, image: null, description: null, } }, endEditing(product) { this.editingItem = null let index = this.products.indexOf(product) let name = product.name let units = product.units let price = product.price let description = product.description axios.put(`/api/products/${product.id}`, {name, units, price, description}) .then(response => this.products[index] = product) }, addProduct(product) { this.addingProduct = null let name = product.name let units = product.units let price = product.price let description = product.description let image = product.image axios.post('/api/products/', {name, units, price, description, image}) .then(response => this.products.push(product)) } } } </script> |
In the methods property we defined the following methods:
newProduct() – called when we want to initiate a new local product object.
endEditing() – called when we are done editing a product.
addProduct() – called when we are want to add a new product.
We imported a ProductModal component, which we will create next. The modal will be used to edit an existing or create a new product. Double clicking on a product listed fires up the modal for editing the product.
Create the ProductModal.vue file in resources/js/components/admin and paste in the following template and style code:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 |
<template> <div class="modal-mask"> <div class="modal-wrapper"> <div class="modal-container"> <div class="modal-header"> <slot name="header" v-html="data.name"></slot> </div> <div class="modal-body"> <slot name="body"> Name: <input type="text" v-model="data.name"> Units: <input type="text" v-model="data.units"> Price: <input type="text" v-model="data.price"> <textarea v-model="data.description" placeholder="description"></textarea> <span > <img :src="data.image" v-show="data.image != null"> <input type="file" id="file" @change="attachFile"> </span> </slot> </div> <div class="modal-footer"> <slot name="footer"> <button class="modal-default-button" @click="uploadFile"> Finish </button> </slot> </div> </div> </div> </div> </template> <script> export default { props: ['product'], data() { return { attachment: null } }, computed: { data: function() { if (this.product != null) { return this.product } return { name: "", units: "", price: "", description: "", image: false } } }, methods: { attachFile(event) { this.attachment = event.target.files[0]; }, uploadFile(event) { if (this.attachment != null) { var formData = new FormData(); formData.append("image", this.attachment) let headers = {'Content-Type': 'multipart/form-data'} axios.post('/api/upload-file', formData, {headers}).then(response => { this.product.image = response.data this.$emit('close', this.product) }) } else { this.$emit('close', this.product) } } } } </script> <style scoped> .modal-mask { position: fixed; z-index: 9998; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, .5); display: table; transition: opacity .3s ease; } .modal-wrapper { display: table-cell; vertical-align: middle; } .modal-container { width: 300px; margin: 0px auto; padding: 20px 30px; background-color: #fff; border-radius: 2px; box-shadow: 0 2px 8px rgba(0, 0, 0, .33); transition: all .3s ease; font-family: Helvetica, Arial, sans-serif; } .modal-header h3 { margin-top: 0; color: #42b983; } .modal-body { margin: 20px 0; } .modal-default-button { float: right; } .modal-enter { opacity: 0; } .modal-leave-active { opacity: 0; } .modal-enter .modal-container, .modal-leave-active .modal-container { -webkit-transform: scale(1.1); transform: scale(1.1); } </style> |
When the modal receives a product’s data, it pre-fills each field with the data. When we attach an image and submit the modal’s form, it is uploaded and the url for the image is returned to us.
We update the image attribute of the product with the url, then emit a close event and return the product with it. If no image is attached, we emit a close event and return the product data along with it.
This modal component is an example provided in Vue documentation here
Payments
There are many payment options for e-commerce platforms. You choose depending on your needs and what is available in your country. Many popular payment processors like Stripe have excellent guides on integrating into a JavaScript or PHP application which you can use. This tutorial won’t cover that though, you can take it as an exercise for practise.
Building our application
We are done building our application. The next thing to do will be to compile our Vue application and serve our Laravel backend. Run the command to build the application.
Admin user:
email: admin@admin.com
name: Admin
password: secret
Create/Register a new user:
email: advcha@yahoo.com
name: Satria
password: satria
Create Laravel CMS
Ref: https://pusher.com/tutorials/cms-laravel-vue-part-1
Install Laravel Installer globally (It’d be better than ‘create project’ and download from the laravel source):
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
teddy@teddy:~/Documents/works/laravel$ composer global require "laravel/installer" Changed current directory to /home/teddy/.composer ./composer.json is not writable. teddy@teddy:~/Documents/works/laravel$ sudo composer global require "laravel/installer" [sudo] password for teddy: Changed current directory to /home/teddy/.composer Do not run Composer as root/super user! See https://getcomposer.org/root for details Using version ^2.0 for laravel/installer ./composer.json has been updated Loading composer repositories with package information Updating dependencies (including require-dev) Nothing to install or update Generating autoload files teddy@teddy:~/Documents/works/laravel$ echo 'export PATH="$HOME/.composer/vendor/bin:$PATH"' >> ~/.bashrc teddy@teddy:~/Documents/works/laravel$ source ~/.bashrc teddy@teddy:~/Documents/works/laravel$ laravel Laravel Installer 2.0.1 Usage: command [options] [arguments] Options: -h, --help Display this help message -q, --quiet Do not output any message -V, --version Display this application version --ansi Force ANSI output --no-ansi Disable ANSI output -n, --no-interaction Do not ask any interactive question -v|vv|vvv, --verbose Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug Available commands: help Displays help for a command list Lists commands new Create a new Laravel application. |
Create the Laravel CMS Project ‘laravel-cms’:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 |
teddy@teddy:~/Documents/works/laravel$ laravel new laravel-cms Crafting application... Loading composer repositories with package information Installing dependencies (including require-dev) from lock file Package operations: 86 installs, 0 updates, 0 removals - Installing doctrine/inflector (v1.3.0): Loading from cache - Installing doctrine/lexer (v1.0.1): Loading from cache - Installing dragonmantank/cron-expression (v2.2.0): Loading from cache - Installing erusev/parsedown (1.7.1): Loading from cache - Installing vlucas/phpdotenv (v2.5.2): Downloading (100%) - Installing symfony/css-selector (v4.2.2): Downloading (100%) - Installing tijsverkoyen/css-to-inline-styles (2.2.1): Loading from cache - Installing symfony/polyfill-php72 (v1.10.0): Downloading (100%) - Installing symfony/polyfill-mbstring (v1.10.0): Loading from cache - Installing symfony/var-dumper (v4.2.2): Downloading (100%) - Installing symfony/routing (v4.2.2): Downloading (100%) - Installing symfony/process (v4.2.2): Downloading (100%) - Installing symfony/polyfill-ctype (v1.10.0): Loading from cache - Installing symfony/http-foundation (v4.2.2): Downloading (100%) - Installing symfony/contracts (v1.0.2): Downloading (100%) - Installing symfony/event-dispatcher (v4.2.2): Downloading (100%) - Installing psr/log (1.1.0): Loading from cache - Installing symfony/debug (v4.2.2): Downloading (100%) - Installing symfony/http-kernel (v4.2.2): Downloading (100%) - Installing symfony/finder (v4.2.2): Downloading (100%) - Installing symfony/console (v4.2.2): Downloading (100%) - Installing egulias/email-validator (2.1.7): Downloading (100%) - Installing swiftmailer/swiftmailer (v6.1.3): Loading from cache - Installing paragonie/random_compat (v9.99.99): Loading from cache - Installing ramsey/uuid (3.8.0): Loading from cache - Installing psr/simple-cache (1.0.1): Loading from cache - Installing psr/container (1.0.0): Loading from cache - Installing opis/closure (3.1.5): Downloading (100%) - Installing symfony/translation (v4.2.2): Downloading (100%) - Installing nesbot/carbon (1.36.2): Loading from cache - Installing monolog/monolog (1.24.0): Loading from cache - Installing league/flysystem (1.0.49): Loading from cache - Installing ralouphie/getallheaders (2.0.5): Loading from cache - Installing psr/http-message (1.0.1): Loading from cache - Installing guzzlehttp/psr7 (1.5.2): Loading from cache - Installing guzzlehttp/promises (v1.3.1): Loading from cache - Installing guzzlehttp/guzzle (6.3.3): Loading from cache - Installing laravel/slack-notification-channel (v1.0.3): Downloading (connectDownloading (100%) - Installing zendframework/zend-diactoros (1.8.6):Downloading (100%) ) - Installing php-http/promise (v1.0.0): Downloading (100%) - Installing php-http/httplug (v1.1.0): Downloading (100%) - Installing php-http/guzzle6-adapter (v1.1.1): Downloading (100%) - Installing lcobucci/jwt (3.2.5): Downloading (100%) - Installing nexmo/client (1.6.0): Downloading (100%) - Installing laravel/nexmo-notification-channel (v1.0.1): Downloading (connectDownloading (100%) - Installing laravel/framework (v5.7.21): Downloading (100%) - Installing fideloper/proxy (4.1.0): Downloading (100%) - Installing jakub-onderka/php-console-color (v0.2): Loading from cache - Installing nikic/php-parser (v4.2.0): Downloading (100%) - Installing jakub-onderka/php-console-highlighter (v0.4): Downloading (connecDownloading (100%) - Installing dnoegel/php-xdg-base-dir (0.1): Loading from cache - Installing psy/psysh (v0.9.9): Downloading (100%) - Installing laravel/tinker (v1.0.8): Downloading (100%) - Installing beyondcode/laravel-dump-server (1.2.2): Downloading (connecting..Downloading (100%) - Installing fzaninotto/faker (v1.8.0): Loading from cache - Installing hamcrest/hamcrest-php (v2.0.0): Loading from cache - Installing mockery/mockery (1.2.0): Loading from cache - Installing filp/whoops (2.3.1): Downloading (100%) - Installing nunomaduro/collision (v2.1.1): Downloading (100%) - Installing webmozart/assert (1.4.0): Loading from cache - Installing phpdocumentor/reflection-common (1.0.1): Loading from cache - Installing phpdocumentor/type-resolver (0.4.0): Loading from cache - Installing phpdocumentor/reflection-docblock (4.3.0): Loading from cache - Installing phpunit/php-token-stream (3.0.1): Downloading (100%) - Installing sebastian/version (2.0.1): Loading from cache - Installing sebastian/resource-operations (2.0.1): Downloading (connecting...Downloading (100%) - Installing sebastian/recursion-context (3.0.0): Loading from cache - Installing sebastian/object-reflector (1.1.1): Loading from cache - Installing sebastian/object-enumerator (3.0.3): Loading from cache - Installing sebastian/global-state (2.0.0): Loading from cache - Installing sebastian/exporter (3.1.0): Loading from cache - Installing sebastian/environment (4.0.1): Downloading (100%) - Installing sebastian/diff (3.0.1): Loading from cache - Installing sebastian/comparator (3.0.2): Loading from cache - Installing phpunit/php-timer (2.0.0): Loading from cache - Installing phpunit/php-text-template (1.2.1): Loading from cache - Installing phpunit/php-file-iterator (2.0.2): Loading from cache - Installing theseer/tokenizer (1.1.0): Loading from cache - Installing sebastian/code-unit-reverse-lookup (1.0.1): Loading from cache - Installing phpunit/php-code-coverage (6.1.4): Downloading (100%) - Installing doctrine/instantiator (1.1.0): Loading from cache - Installing phpspec/prophecy (1.8.0): Loading from cache - Installing phar-io/version (2.0.1): Loading from cache - Installing phar-io/manifest (1.0.3): Loading from cache - Installing myclabs/deep-copy (1.8.1): Loading from cache - Installing phpunit/phpunit (7.5.2): Downloading (100%) symfony/routing suggests installing doctrine/annotations (For using the annotation loader) symfony/routing suggests installing symfony/config (For using the all-in-one router or any loader) symfony/routing suggests installing symfony/dependency-injection (For loading routes from a service) symfony/routing suggests installing symfony/expression-language (For using expression matching) symfony/routing suggests installing symfony/yaml (For using the YAML loader) symfony/contracts suggests installing psr/cache (When using the Cache contracts) symfony/contracts suggests installing symfony/cache-contracts-implementation () symfony/contracts suggests installing symfony/service-contracts-implementation () symfony/event-dispatcher suggests installing symfony/dependency-injection () symfony/http-kernel suggests installing symfony/browser-kit () symfony/http-kernel suggests installing symfony/config () symfony/http-kernel suggests installing symfony/dependency-injection () symfony/console suggests installing symfony/lock () swiftmailer/swiftmailer suggests installing true/punycode (Needed to support internationalized email addresses, if ext-intl is not installed) paragonie/random_compat suggests installing ext-libsodium (Provides a modern crypto API that can be used to generate random bytes.) ramsey/uuid suggests installing ext-libsodium (Provides the PECL libsodium extension for use with the SodiumRandomGenerator) ramsey/uuid suggests installing ext-uuid (Provides the PECL UUID extension for use with the PeclUuidTimeGenerator and PeclUuidRandomGenerator) ramsey/uuid suggests installing ircmaxell/random-lib (Provides RandomLib for use with the RandomLibAdapter) ramsey/uuid suggests installing moontoast/math (Provides support for converting UUID to 128-bit integer (in string form).) ramsey/uuid suggests installing ramsey/uuid-console (A console application for generating UUIDs with ramsey/uuid) ramsey/uuid suggests installing ramsey/uuid-doctrine (Allows the use of Ramsey\Uuid\Uuid as Doctrine field type.) symfony/translation suggests installing symfony/config () symfony/translation suggests installing symfony/yaml () nesbot/carbon suggests installing friendsofphp/php-cs-fixer (Needed for the `composer phpcs` command. Allow to automatically fix code style.) nesbot/carbon suggests installing phpstan/phpstan (Needed for the `composer phpstan` command. Allow to detect potential errors.) monolog/monolog suggests installing aws/aws-sdk-php (Allow sending log messages to AWS services like DynamoDB) monolog/monolog suggests installing doctrine/couchdb (Allow sending log messages to a CouchDB server) monolog/monolog suggests installing ext-amqp (Allow sending log messages to an AMQP server (1.0+ required)) monolog/monolog suggests installing ext-mongo (Allow sending log messages to a MongoDB server) monolog/monolog suggests installing graylog2/gelf-php (Allow sending log messages to a GrayLog2 server) monolog/monolog suggests installing mongodb/mongodb (Allow sending log messages to a MongoDB server via PHP Driver) monolog/monolog suggests installing php-amqplib/php-amqplib (Allow sending log messages to an AMQP server using php-amqplib) monolog/monolog suggests installing php-console/php-console (Allow sending log messages to Google Chrome) monolog/monolog suggests installing rollbar/rollbar (Allow sending log messages to Rollbar) monolog/monolog suggests installing ruflin/elastica (Allow sending log messages to an Elastic Search server) monolog/monolog suggests installing sentry/sentry (Allow sending log messages to a Sentry server) league/flysystem suggests installing league/flysystem-aws-s3-v2 (Allows you to use S3 storage with AWS SDK v2) league/flysystem suggests installing league/flysystem-aws-s3-v3 (Allows you to use S3 storage with AWS SDK v3) league/flysystem suggests installing league/flysystem-azure (Allows you to use Windows Azure Blob storage) league/flysystem suggests installing league/flysystem-cached-adapter (Flysystem adapter decorator for metadata caching) league/flysystem suggests installing league/flysystem-eventable-filesystem (Allows you to use EventableFilesystem) league/flysystem suggests installing league/flysystem-rackspace (Allows you to use Rackspace Cloud Files) league/flysystem suggests installing league/flysystem-sftp (Allows you to use SFTP server storage via phpseclib) league/flysystem suggests installing league/flysystem-webdav (Allows you to use WebDAV storage) league/flysystem suggests installing league/flysystem-ziparchive (Allows you to use ZipArchive adapter) league/flysystem suggests installing spatie/flysystem-dropbox (Allows you to use Dropbox storage) league/flysystem suggests installing srmklive/flysystem-dropbox-v2 (Allows you to use Dropbox storage for PHP 5 applications) lcobucci/jwt suggests installing mdanter/ecc (Required to use Elliptic Curves based algorithms.) laravel/framework suggests installing aws/aws-sdk-php (Required to use the SQS queue driver and SES mail driver (^3.0).) laravel/framework suggests installing doctrine/dbal (Required to rename columns and drop SQLite columns (^2.6).) laravel/framework suggests installing league/flysystem-aws-s3-v3 (Required to use the Flysystem S3 driver (^1.0).) laravel/framework suggests installing league/flysystem-cached-adapter (Required to use the Flysystem cache (^1.0).) laravel/framework suggests installing league/flysystem-rackspace (Required to use the Flysystem Rackspace driver (^1.0).) laravel/framework suggests installing league/flysystem-sftp (Required to use the Flysystem SFTP driver (^1.0).) laravel/framework suggests installing moontoast/math (Required to use ordered UUIDs (^1.1).) laravel/framework suggests installing pda/pheanstalk (Required to use the beanstalk queue driver (^3.0).) laravel/framework suggests installing predis/predis (Required to use the redis cache and queue drivers (^1.0).) laravel/framework suggests installing pusher/pusher-php-server (Required to use the Pusher broadcast driver (^3.0).) laravel/framework suggests installing symfony/dom-crawler (Required to use most of the crawler integration testing tools (^4.1).) laravel/framework suggests installing symfony/psr-http-message-bridge (Required to psr7 bridging features (^1.0).) psy/psysh suggests installing ext-pdo-sqlite (The doc command requires SQLite to work.) psy/psysh suggests installing hoa/console (A pure PHP readline implementation. You'll want this if your PHP install doesn't already support readline or libedit.) filp/whoops suggests installing whoops/soap (Formats errors as SOAP responses) sebastian/global-state suggests installing ext-uopz (*) phpunit/phpunit suggests installing ext-soap (*) phpunit/phpunit suggests installing phpunit/php-invoker (^2.0) Generating optimized autoload files > @php -r "file_exists('.env') || copy('.env.example', '.env');" > @php artisan key:generate --ansi Application key set successfully. > Illuminate\Foundation\ComposerScripts::postAutoloadDump > @php artisan package:discover --ansi Discovered Package: beyondcode/laravel-dump-server Discovered Package: fideloper/proxy Discovered Package: laravel/nexmo-notification-channel Discovered Package: laravel/slack-notification-channel Discovered Package: laravel/tinker Discovered Package: nesbot/carbon Discovered Package: nunomaduro/collision Package manifest generated successfully. Application ready! Build something amazing. |
Go to the project directory and check the laravel version:
|
1 2 3 |
teddy@teddy:~/Documents/works/laravel$ cd laravel-cms/ teddy@teddy:~/Documents/works/laravel/laravel-cms$ php artisan help -V Laravel Framework 5.7.21 |
Run it for the first time:
|
1 |
teddy@teddy:~/Documents/works/laravel/laravel-cms$ php artisan serve |
Open it on your browser : http://localhost:8000/
Set up the database (MySql)
Create a new database ‘laravel_cms’
then modify .env file to match your mysql server setting and database
SETTING UP USER ROLES
Like most content management systems, we are going to have a user role system so that our blog can have multiple types of users; the admin and regular user. The admin should be able to create a post and perform other CRUD operations on a post. The regular user, on the other hand, should be able to view and comment on a post.
For us to implement this functionality, we need to implement user authentication and add a simple role authorization system.
SETTING UP USER AUTHENTICATION
Laravel provides user authentication out of the box, which is great, and we can key into the feature by running a single command:
|
1 2 |
teddy@teddy:~/Documents/works/laravel/laravel-cms$ php artisan make:auth Authentication scaffolding generated successfully. |
The above will create all that’s necessary for authentication in our application so we do not need to do anything extra.
SETTING UP ROLE AUTHORIZATION
We need a model for the user roles so let’s create one and an associated migration file:
|
1 2 3 |
teddy@teddy:~/Documents/works/laravel/laravel-cms$ php artisan make:model Role -m Model created successfully. Created Migration: 2019_01_21_031440_create_roles_table |
In the database/migrations folder, find the newly created migration file and update the CreateRolesTable class (2019_01_21_031440_create_roles_table.php) with this snippet:
Update with adding two new fields: $table->string(‘name’) and
$table->string(‘description’):
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
//laravel-cms/database/migrations/2019_01_21_031440_create_roles_table.php use Illuminate\Support\Facades\Schema; use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Migrations\Migration; class CreateRolesTable extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::create('roles', function (Blueprint $table) { $table->increments('id'); $table->string('name'); $table->string('description'); $table->timestamps(); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::dropIfExists('roles'); } } |
We intend to create a many-to-many relationship between the User and Role models so let’s add a relationship method on both models.
Open the User model and add the following method:
|
1 2 3 4 |
//laravel/laravel-cms/app/User.php public function roles(){ return $this->belongsToMany(Role::class); } |
Open the Role model and include the following method:
|
1 2 3 4 |
//laravel-cms/app/Role.php public function users(){ return $this->belongsToMany(User::class); } |
We are also going to need a pivot table to associate each user with a matching role so let’s create a new migration file for the role_user table:
|
1 2 |
teddy@teddy:~/Documents/works/laravel/laravel-cms$ php artisan make:migration create_role_user_table Created Migration: 2019_01_21_032646_create_role_user_table |
In the database/migrations folder, find the newly created migration file and update the CreateRoleUserTable class with this snippet:
|
1 2 3 4 5 6 7 8 9 10 |
//laravel-cms/database/migrations/2019_01_21_032646_create_role_user_table.php public function up() { Schema::create('role_user', function (Blueprint $table) { $table->increments('id'); $table->integer('role_id')->unsigned(); $table->integer('user_id')->unsigned(); //$table->timestamps(); }); } |
Next, let’s create seeders that will populate the users and roles tables with some data. In your terminal, run the following command to create the database seeders:
|
1 2 3 4 |
teddy@teddy:~/Documents/works/laravel/laravel-cms$ php artisan make:seeder RoleTableSeeder Seeder created successfully. teddy@teddy:~/Documents/works/laravel/laravel-cms$ php artisan make:seeder UserTableSeeder Seeder created successfully. |
In the database/seeds folder, open the RoleTableSeeder.php file and replace the contents with the following code:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
//laravel-cms/database/seeds/RoleTableSeeder.php use Illuminate\Database\Seeder; use App\Role; class RoleTableSeeder extends Seeder { /** * Run the database seeds. * * @return void */ public function run() { $role_regular_user = new Role; $role_regular_user->name = 'user'; $role_regular_user->description = 'A regular user'; $role_regular_user->save(); $role_admin_user = new Role; $role_admin_user->name = 'admin'; $role_admin_user->description = 'An admin user'; $role_admin_user->save(); } } |
Open the UserTableSeeder.php file and replace the contents with the following code:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
//laravel-cms/database/seeds/UserTableSeeder.php use Illuminate\Database\Seeder; use Illuminate\Support\Facades\Hash; use App\User; use App\Role; class UserTableSeeder extends Seeder { /** * Run the database seeds. * * @return void */ public function run() { $user = new User; $user->name = 'Satria Faestha'; $user->email = 'satriaf@yahoo.com'; $user->password = bcrypt('teddy'); $user->save(); $user->roles()->attach(Role::where('name', 'user')->first()); $admin = new User; $admin->name = 'Admin'; $admin->email = 'advcha@yahoo.com'; $admin->password = bcrypt('teddy'); $admin->save(); $admin->roles()->attach(Role::where('name', 'admin')->first()); } } |
We also need to update the DatabaseSeeder class. Open the file and update the run method as seen below:
|
1 2 3 4 5 6 7 8 |
//laravel-cms/database/seeds/DatabaseSeeder.php public function run() { $this->call([ RoleTableSeeder::class, UserTableSeeder::class, ]); } |
Next, let’s update the User model. We will be adding a checkRoles method that checks what role a user has. We will return a 404 page where a user doesn’t have the expected role for a page. Open the User model and add these methods:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
//laravel-cms/app/User.php public function checkRoles($roles){ if(!is_array($roles)){ $roles = [$roles]; } if(!$this->hasAnyRole($roles)){ auth()->logout(); abort(404); } } public function hasAnyRole($roles):bool { return (bool) $this->roles()->whereIn('name', $roles)->first(); } public function hasRole($role):bool { return (bool)$this->roles()->where('name', $role)->first(); } |
Let’s modify the RegisterController.php file in the Controllers/Auth folder so that a default role, the user role, is always attached to a new user at registration.
Open the RegisterController and update the create action with the following code:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
//laravel-cms/app/Http/Controllers/Auth/RegisterController.php protected function create(array $data) { /*return User::create([ 'name' => $data['name'], 'email' => $data['email'], 'password' => Hash::make($data['password']), ]);*/ $user = User::create([ 'name' => $data['name'], 'email' => $data['email'], 'password' => Hash::make($data['password']), ]); $user->roles()->attach(\App\Role::where('name', 'user')->first()); return $user; } |
Now let’s migrate and seed the database so that we can log in with the sample accounts. To do this, run the following command in your terminal:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
teddy@teddy:~/Documents/works/laravel/laravel-cms$ php artisan migrate:fresh --seed Dropped all tables successfully. Migration table created successfully. Migrating: 2014_10_12_000000_create_users_table Migrated: 2014_10_12_000000_create_users_table Migrating: 2014_10_12_100000_create_password_resets_table Migrated: 2014_10_12_100000_create_password_resets_table Migrating: 2019_01_21_031440_create_roles_table Illuminate\Database\QueryException : SQLSTATE[42S21]: Column already exists: 1060 Duplicate column name 'created_at' (SQL: create table `roles` (`created_at` timestamp null, `updated_at` timestamp null, `name` varchar(255) not null, `description` varchar(255) not null, `created_at` timestamp null, `updated_at` timestamp null) default character set utf8mb4 collate 'utf8mb4_unicode_ci') at /home/teddy/Documents/works/laravel/laravel-cms/vendor/laravel/framework/src/Illuminate/Database/Connection.php:664 660| // If an exception occurs when attempting to run a query, we'll format the error 661| // message to include the bindings with SQL, which will make this exception a 662| // lot more helpful to the developer instead of just the database's errors. 663| catch (Exception $e) { > 664| throw new QueryException( 665| $query, $this->prepareBindings($bindings), $e 666| ); 667| } 668| Exception trace: 1 PDOException::("SQLSTATE[42S21]: Column already exists: 1060 Duplicate column name 'created_at'") /home/teddy/Documents/works/laravel/laravel-cms/vendor/laravel/framework/src/Illuminate/Database/Connection.php:458 2 PDOStatement::execute() /home/teddy/Documents/works/laravel/laravel-cms/vendor/laravel/framework/src/Illuminate/Database/Connection.php:458 Please use the argument -v to see more details. |
SOLUTION: check again ‘CreateRolesTable’ Class in /home/teddy/Documents/works/laravel/laravel-cms/database/migrations/2019_01_21_031440_create_roles_table.php. Somehow the ‘up’ function is changed!!!
|
1 |
Illuminate\Database\QueryException : SQLSTATE[23000]: Integrity constraint violation: 1048 Column 'role_id' cannot be null (SQL: insert into `role_user` (`role_id`, `user_id`) values (, 1)) |
SOLUTION: It’s related with the above problem!
This is working:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
teddy@teddy:~/Documents/works/laravel/laravel-cms$ php artisan migrate:fresh --seed Dropped all tables successfully. Migration table created successfully. Migrating: 2014_10_12_000000_create_users_table Migrated: 2014_10_12_000000_create_users_table Migrating: 2014_10_12_100000_create_password_resets_table Migrated: 2014_10_12_100000_create_password_resets_table Migrating: 2019_01_21_031440_create_roles_table Migrated: 2019_01_21_031440_create_roles_table Migrating: 2019_01_21_032646_create_role_user_table Migrated: 2019_01_21_032646_create_role_user_table Seeding: RoleTableSeeder Seeding: UserTableSeeder Database seeding completed successfully. |
In order to test that our roles work as they should, we will make an update to the HomeController.php file. Open the HomeController and update the index method as seen below:
|
1 2 3 4 5 6 7 |
//laravel-cms/app/Http/Controllers/HomeController.php public function index(Request $request) { $request->user()->checkRoles('admin'); return view('home'); } |
Now, only administrators should be able to see the dashboard. In a more complex application, we would use a middleware to do this instead.
We can test that this works by serving the application and logging (http://localhost:8000/login) in both user accounts: satriaf@yahoo.com (user) and advcha@yahoo.com (admin).
Remember that in our UserTableSeeder.php file, we defined satriaf@yahoo.com as a regular user and advcha@yahoo.com as an admin, so satriaf@yahoo.com should see a 404 error after logging in and advcha@yahoo.com should be able to see the homepage. Also http://localhost:8000/home would be redirected to http://localhost:8000/login if none logged in!
We will also confirm that whenever a new user registers, he is assigned a role and it is the role of a regular user. We will create a new user and call him user1@yahoo.com (password: user1_123) , he should see a 404 error right after:
It works just as we wanted it to, however, it doesn’t really make any sense for us to redirect a regular user to a 404 page. Instead, we will edit the HomeController so that it redirects users based on their roles, that is, it redirects a regular user to a regular homepage and an admin to an admin dashboard.
Open the HomeController.php file and update the index method as seen below:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
//laravel-cms/app/Http/Controllers/HomeController.php public function index(Request $request) { //$request->user()->checkRoles('admin'); if($request->user()->hasRole('user')){ return redirect('/'); } if($request->user()->hasRole('admin')){ return redirect('/admin/dashboard'); } //return view('home'); } |
After we login as an user, we missed the logout link here. So modify the main template welcome.blade.php to add the logout link:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
//laravel-cms/resources/views/welcome.blade.php @if (Route::has('login')) <div class="top-right links"> @auth <a href="{{ url('/home') }}">Home</a> <a href="{{ url('/logout') }}">Logout</a> @else <a href="{{ route('login') }}">Login</a> @if (Route::has('register')) <a href="{{ route('register') }}">Register</a> @endif @endauth </div> @endif |
But if we click the logout link, it’d show an error:
|
1 2 |
Symfony \ Component \ HttpKernel \ Exception \ MethodNotAllowedHttpException No message |
It means we need to add a route for ‘logout’. Add the route in web.php:
|
1 2 |
//laravel/laravel-cms/routes/web.php Route::get('/logout', 'Auth\LoginController@logout')->name('logout' ); |
Ref: https://stackoverflow.com/questions/44716379/laravel-5-4-24-throws-methodnotallowedhttpexception-during-logout-of-users
If we serve our application and try to log in using the admin account, we will hit a 404 error because we do not have a controller or a view for the admin/dashboard route. Let’s create the AdminController with this command:
|
1 2 |
teddy@teddy:~/Documents/works/laravel/laravel-cms$ php artisan make:controller AdminController Controller created successfully. |
We will add the /admin/ route to our routes/web.php file:
|
1 2 |
//laravel-cms/routes/web.php Route::get('/admin/{any}', 'AdminController@index')->where('any', '.*'); |
Note that we wrote /admin/{any} here because we intend to serve every page of the admin dashboard using the Vue router. When we start building the admin dashboard in the next article, we will let Vue handle all the routes of the /admin pages.
Let’s update the AdminController.php file to use the auth middleware and include an index() action method:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
//laravel-cms/app/Http/Controllers/AdminController.php class AdminController extends Controller { public function __construct(){ $this->middleware('auth'); } public function index(){ if(request()->user()->hasRole('admin')){ return view('admin.dashboard'); } if(request()->user()->hasRole('user')){ return redirect('/'); } } } |
In the index() action method, we included a snippet that will ensure that only admin users can visit the admin dashboard and perform CRUD operations on posts. If we try to login as an admin, it’d show this error:
|
1 2 |
InvalidArgumentException View [admin.dashboard] not found. |
Because we haven’t create any view for the admin route. We’ll create it with vue router later.
Here, we will create the Post model and start building the frontend for the application.
Our application allows different levels of accessibility for two kinds of users; the regular user and admin. Now, we will focus on building the view that the regular users are permitted to see.
Before we build any views, let’s create the Post model as it is imperative to rendering the view. We will create the Post model with an associated resource controller and a migration file using this command:
|
1 2 3 4 |
teddy@teddy:~/Documents/works/laravel/laravel-cms$ php artisan make:model Post -mr Model created successfully. Created Migration: 2019_01_21_083457_create_posts_table Controller created successfully. |
We added the r flag because we want the controller to be a resource controller. The m flag will generate a migration for the model.
Let’s navigate into the database/migrations folder and update the CreatePostsTable class that was generated for us:
|
1 2 3 4 5 6 7 8 9 10 11 12 |
//laravel-cms/database/migrations/2019_01_21_083457_create_posts_table.php public function up() { Schema::create('posts', function (Blueprint $table) { $table->increments('id'); $table->integer('user_id')->unsigned(); $table->string('title'); $table->text('body'); $table->binary('image')->nullable(); $table->timestamps(); }); } |
We included a user_id property because we want to create a relationship between the User and Post models. A Post also has an image field, which is where its associated image’s address will be stored.
CREATING A DATABASE SEEDER FOR THE POST TABLE
We will create a new seeder file for the posts table using this command:
|
1 2 |
teddy@teddy:~/Documents/works/laravel/laravel-cms$ php artisan make:seeder PostTableSeeder Seeder created successfully. |
Let’s navigate into the database/seeds folder and update the PostTableSeeder.php file:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
//laravel-cms/database/seeds/PostTableSeeder.php public function run() { $post = new Post; $post->user_id = 2; $post->title = "Using Laravel Seeders"; $post->body = "Laravel includes a simple method of seeding your database with test data using seed classes. All seed classes are stored in the database/seeds directory. Seed classes may have any name you wish, but probably should follow some sensible convention, such as UsersTableSeeder, etc. By default, a DatabaseSeeder class is defined for you. From this class, you may use the call method to run other seed classes, allowing you to control the seeding order."; $post->save(); $post = new Post; $post->user_id = 2; $post->title = "Database: Migrations"; $post->body = "Migrations are like version control for your database, allowing your team to easily modify and share the application's database schema. Migrations are typically paired with Laravel's schema builder to easily build your application's database schema. If you have ever had to tell a teammate to manually add a column to their local database schema, you've faced the problem that database migrations solve."; $post->save(); } |
Let’s open the DatabaseSeeder and update it with the following code:
|
1 2 3 4 5 6 7 8 9 |
//laravel-cms/database/seeds/DatabaseSeeder.php public function run() { $this->call([ RoleTableSeeder::class, UserTableSeeder::class, PostTableSeeder::class, ]); } |
When we run this seeder, it will create two new posts and assign both of them to the admin user whose ID is 2. We are attaching both posts to the admin user because the regular users are only allowed to view posts and make comments; they can’t create a post. We will use this command to migrate our tables and seed the database:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
teddy@teddy:~/Documents/works/laravel/laravel-cms$ php artisan migrate:fresh --seed Dropped all tables successfully. Migration table created successfully. Migrating: 2014_10_12_000000_create_users_table Migrated: 2014_10_12_000000_create_users_table Migrating: 2014_10_12_100000_create_password_resets_table Migrated: 2014_10_12_100000_create_password_resets_table Migrating: 2019_01_21_031440_create_roles_table Migrated: 2019_01_21_031440_create_roles_table Migrating: 2019_01_21_032646_create_role_user_table Migrated: 2019_01_21_032646_create_role_user_table Migrating: 2019_01_21_083457_create_posts_table Migrated: 2019_01_21_083457_create_posts_table Seeding: RoleTableSeeder Seeding: UserTableSeeder Seeding: PostTableSeeder Database seeding completed successfully. |
DEFINING THE RELATIONSHIPS
Just as we previously created a many-to-many relationship between the User and Role models, we need to create a different kind of relationship between the Post and User models.
We will define the relationship as a one-to-many relationship because a user will have many posts but a post will only ever belong to one user.
Open the User model and include the method below:
|
1 2 3 4 5 |
// File: ./app/User.php public function posts() { return $this->hasMany(Post::class); } |
Open the Post model and include the method below:
|
1 2 3 4 5 |
// File: ./app/Post.php public function user() { return $this->belongsTo(User::class); } |
SETTING UP THE ROUTES
At this point in our application, we do not have a front page with all the posts listed. Let’s create so anyone can see all of the created posts. Asides from the front page, we also need a single post page in case a user needs to read a specific post.
Let’s include two new routes to our routes/web.php file:
The first route will match requests to the root of our application and will be handled by the PostController@all action:
|
1 |
Route::get('/', 'PostController@all'); |
In the routes/web.php file, there will already be a route definition for the / address, you will have to replace it with the new route definition above.
The second route will handle requests for specific Post items and will be handled by the PostController@single action:
|
1 |
Route::get('/posts/{post}', 'PostController@single'); |
With these two new routes added, here’s what the routes/web.php file should look like this:
|
1 2 3 4 5 6 7 8 9 10 11 12 |
//laravel-cms/routes/web.php /*Route::get('/', function () { return view('welcome'); });*/ Auth::routes(); Route::get('/home', 'HomeController@index')->name('home'); Route::get('/logout', 'Auth\LoginController@logout')->name('logout' ); Route::get('/', PostController@all); Route::get('/posts/{post}', PostController@single); |
SETTING UP THE POST CONTROLLER
In this section, we want to define the handler action methods that we registered in the routes/web.php file so that our application know how to render the matching views.
First, let’s add the all() method:
|
1 2 3 4 5 6 7 |
// File: ./app/Http/Controllers/PostController.php public function all() { return view('landing', [ 'posts' => Post::latest()->paginate(5) ]); } |
Here, we want to retrieve five created posts per page and send to the landing view. We will create this view shortly.
Next, let’s add the single() method to the controller:
|
1 2 3 4 5 |
// File: ./app/Http/Controllers/PostController.php public function single(Post $post) { return view('single', compact('post')); } |
In the method above, we used a feature of Laravel named route model binding to map the URL parameter to a Post instance with the same ID. We are returning a single view, which we will create shortly. This will be the view for the single post page.
BUILDING OUR VIEWS
Laravel uses a templating engine called Blade for its frontend. We will use Blade to build these parts of the frontend before switching to Vue in the next chapter.
Navigate to the resources/views folder and create two new Blade files. They are landing.blade.php and single.blade.php
These are the files that will load the views for the landing page and single post page. Before we start writing any code in these files, we want to create a simple layout template that our page views can use as a base.
In the resources/views/layouts folder, create a Blade template file and call it master.blade.php. This is where we will define the inheritable template for our single and landing pages.
Open the master.blade.php file and update it with this code:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 |
<!-- File: ./resources/views/layouts/master.blade.php --> <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <meta name="description" content=""> <meta name="author" content="Satria Faestha"> <title>LaravelCMS</title> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css"> <style> body { padding-top: 54px; } @media (min-width: 992px) { body { padding-top: 56px; } } </style> </head> <body> <nav class="navbar navbar-expand-lg navbar-dark bg-dark fixed-top"> <div class="container"> <a class="navbar-brand" href="/">LaravelCMS</a> <div class="collapse navbar-collapse" id="navbarResponsive"> <ul class="navbar-nav ml-auto"> @if (Route::has('login')) @auth <li class="nav-item"> <a class="nav-link" href="{{ url('/home') }}">Home</a> </li> <li class="nav-item"> <a class="nav-link" href="{{ route('logout') }}" onclick="event.preventDefault();document.getElementById('logout-form').submit();"> Log out ({{ Auth::user()->name }}) </a> <form id="logout-form" action="{{ route('logout') }}" method="POST" style="display: none;"> @csrf </form> </li> @else <li class="nav-item"> <a class="nav-link" href="{{ route('login') }}">Login</a> </li> <li class="nav-item"> <a class="nav-link" href="{{ route('register') }}">Register</a> </li> @endauth @endif </ul> </div> </div> </nav> <div id="app"> @yield('content') </div> <footer class="py-5 bg-dark"> <div class="container"> <p class="m-0 text-center text-white">Copyright © LaravelCMS 2018</p> </div> </footer> </body> </html> |
Now we can inherit this template in the landing.blade.php file, open it and update it with this code:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
{{-- File: ./resources/views/landing.blade.php --}} @extends('layouts.master') @section('content') <div class="container"> <div class="row align-items-center"> <div class="col-md-8 mx-auto"> <h1 class="my-4 text-center">Welcome to the Blog </h1> @foreach ($posts as $post) <div class="card mb-4"> <img class="card-img-top" src=" {!! !empty($post->image) ? '/uploads/posts/' . $post->image : 'http://placehold.it/750x300' !!} " alt="Card image cap"> <div class="card-body"> <h2 class="card-title text-center">{{ $post->title }}</h2> <p class="card-text"> {{ str_limit($post->body, $limit = 280, $end = '...') }} </p> <a href="/posts/{{ $post->id }}" class="btn btn-primary">Read More →</a> </div> <div class="card-footer text-muted"> Posted {{ $post->created_at->diffForHumans() }} by <a href="#">{{ $post->user->name }} </a> </div> </div> @endforeach </div> </div> </div> @endsection |
Let’s do the same with the single.blade.php file, open it and update it with this code:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
{{-- File: ./resources/views/single.blade.php --}} @extends('layouts.master') @section('content') <div class="container"> <div class="row"> <div class="col-lg-10 mx-auto"> <h3 class="mt-4">{{ $post->title }} <span class="lead"> by <a href="#"> {{ $post->user->name }} </a></span> </h3> <hr> <p>Posted {{ $post->created_at->diffForHumans() }} </p> <hr> <img class="img-fluid rounded" src=" {!! !empty($post->image) ? '/uploads/posts/' . $post->image : 'http://placehold.it/750x300' !!} " alt=""> <hr> <p class="lead">{{ $post->body }}</p> <hr> <div class="card my-4"> <h5 class="card-header">Leave a Comment:</h5> <div class="card-body"> <form> <div class="form-group"> <textarea class="form-control" rows="3"></textarea> </div> <button type="submit" class="btn btn-primary">Submit</button> </form> </div> </div> </div> </div> </div> @endsection |
TESTING THE APPLICATION
We can test the application to see that things work as we expect. When we serve the application, we expect to see a landing page and a single post page. We also expect to see two posts because that’s the number of posts we seeded into the database.
We will serve the application using this command:
|
1 |
php artisan serve |
We have used simple placeholder images here because we haven’t built the admin dashboard that allows CRUD operations to be performed on posts.
In the coming chapters, we will add the ability for an admin to include a custom image when creating a new post.
CONCLUSION
In this chapter, we created the Post model and defined a relationship on it to the User model. We also built the landing page and single page.
We will start building the admin frontend of the CMS. This is the first part of the series where we will integrate Vue and explore Vue’s magical abilities.
Laravel ships with Vue out of the box so we do not need to use the Vue-CLI or reference Vue from a CDN. Please see package.json file on the project root.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
//laravel-cms/package.json "devDependencies": { "axios": "^0.18", "bootstrap": "^4.0.0", "cross-env": "^5.1", "jquery": "^3.2", "laravel-mix": "^4.0.7", "lodash": "^4.17.5", "popper.js": "^1.12", "resolve-url-loader": "^2.3.1", "sass": "^1.15.2", "sass-loader": "^7.1.0", "vue": "^2.5.17" }, |
This makes it possible for us to have all of our application, the frontend, and backend, in a single codebase.
Every newly created instance of a Laravel installation has some Vue files included by default, we can see these files when we navigate into the resources/js/components folder.
SETTING UP VUE AND VUEROUTER
Before we can start using Vue in our application, we need to first install some dependencies using NPM. To install the dependencies that come by default with Laravel, run the command below:
|
1 |
npm install |
We will be managing all of the routes for the admin dashboard using vue-router so let’s pull it in:
|
1 |
npm install --save vue-router |
When the installation is complete, the next thing we want to do is open the resources/js/app.js file and replace its contents with the code below:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
// File: ./resources/js/app.js require('./bootstrap'); import Vue from 'vue' import VueRouter from 'vue-router' import Homepage from './components/Homepage' import Read from './components/Read' Vue.use(VueRouter) const router = new VueRouter({ mode: 'history', routes: [ { path: '/admin/dashboard', name: 'read', component: Read, props: true }, ], }); const app = new Vue({ el: '#app', router, components: { Homepage }, }); |
In the snippet above, we imported the VueRouter and added it to the Vue application. We also imported a Homepage and a Read component. These are the components where we will write our markup so let’s create both files.
NOTE: IF YOU EDIT THE JS/VUE TEMPLATE, DON’T FORGET TO STOP ‘php artisan serve’ THEN RUN ‘npm run dev’ THEN START AGAIN ‘php artisan serve’. IF NOT, YOU’D NOT SEE THE CHANGES YOU MADE!!!
Open the resources/js/components folder and create four files:
Homepage.vue – this will be the parent component for the admin dashboard frontend.
Read.vue – this will be component that displays all the available posts on the admin dashboard.
Create.vue – this will be the component where an admin user can create a new post.
Update.vue – this will be the component that displays the view where an admin user can update an existing post.
Note that we didn’t create a component file for the delete operation, this is because it is going to be possible to delete a post from the Read component. There is no need for a view.
In the resources/js/app.js file, we defined a routes array and in it, we registered a read route. During render time, this route’s path will be mapped to the Read component.
In the previous article, we specified that admin users should be shown an admin.dashboard view in the index method, however, we didn’t create this view. Let’s create the view. Open the resources/views folder and create a new folder called admin. Within the new resources/views/admin folder, create a new file and called dashboard.blade.php. This is going to be the entry point to the admin dashboard, further from this route, we will let the Vue Router handle everything else.
Open the resources/views/admin/dashboard.blade.php file and paste in the following code:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
<!-- File: ./resources/views/admin/dashboard.blade.php --> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title> Welcome to the Admin dashboard </title> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css"> <style> html, body { background-color: #202B33; color: #738491; font-family: "Open Sans"; font-size: 16px; font-smoothing: antialiased; overflow: hidden; } </style> </head> <body> <script src="{{ asset('js/app.js') }}"></script> </body> </html> |
Our goal here is to integrate Vue into the application, so we included the resources/js/app.js file with this line of code:
|
1 |
<script src="{{ asset('js/app.js') }}"></script> |
For our app to work, we need a root element to bind our Vue instance unto. Before the <script> tag, add this snippet of code:
|
1 2 3 4 5 6 7 8 9 10 11 |
... <body> <div id="app"> <Homepage :user-name='@json(auth()->user()->name)' :user-id='@json(auth()->user()->id)'> </Homepage> </div> <script src="{{ asset('js/app.js') }}"></script> </body> ... |
We earlier defined the Homepage component as the wrapping component, that’s why we pulled it in here as the root component. For some of the frontend components to work correctly, we require some details of the logged in admin user to perform CRUD operations. This is why we passed down the userName (:user-name) and userId (:user-id) props to the Homepage component.
We need to prevent the CSRF error from occurring in our Vue frontend, so include this snippet of code just before the <title> tag:
|
1 2 |
<meta name="csrf-token" content="{{ csrf_token() }}"> <script> window.Laravel = { csrfToken: 'csrf_token() ' } </script> |
This snippet will ensure that the correct token is always included in our frontend, Laravel provides the CSRFprotection for us out of the box.
SETTING UP THE HOMEPAGE VIEW
Open the Homepage.vue file that we created some time ago and include this markup template:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
<!-- File: ./resources/js/components/Homepage.vue --> <template> <div> <nav> <section> <a style="color: white" href="/admin/dashboard">Laravel-CMS</a> || <a style="color: white" href="/">HOME</a> <hr> <ul> <li> <router-link :to="{ name: 'create', params: { userId } }"> NEW POST </router-link> </li> </ul> </section> </nav> <article> <header> <header class="d-inline">Welcome, {{ userName }}</header> <p @click="logout" class="float-right mr-3" style="cursor: pointer">Logout</p> </header> <div> <router-view></router-view> </div> </article> </div> </template> |
We added a router-link in this template, which routes to the Create component.
We are passing the userId data to the create component because a userId is required during Post creation.
UPDATED: I want to pass more variables (like user-email and user-admin. DON’T USE SINGLE WORD VARIABLE LIKE title. USE DASH SEPARATED VARIABLE) to vue template. So modify resources/views/admin/dashboard.blade.php:
|
1 2 3 4 5 6 7 8 9 10 11 |
<body> <div id="app"> <Homepage :user-name='@json(auth()->user()->name)' :user-id='@json(auth()->user()->id)' :user-email='@json(auth()->user()->email)' :user-admin='@json(auth()->user()->hasRole("admin"))'> </Homepage> </div> <script src="{{ asset('js/app.js') }}"></script> </body> |
Then catch the variables in resources/js/components/Homepage.vue:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
<script> export default { name: "Homepage", props: { userId: { type: Number, required: true }, userName: { type: String, required: true }, userEmail: { type: String, required: true }, userAdmin: { type: Boolean, required: true } }, ... |
Here is the passing variables in Vue chrome toolbars
How to get user role name on the vue template???
I need to create a helper function in User model (app/User.php):
|
1 2 3 |
public function getRole(){ return $this->roles()->first()->name; } |
Ref: https://stackoverflow.com/questions/36719279/laravel-5-2-how-to-get-the-role-of-current-user-when-using-zizaco-entrust
Then modify again resources/views/admin/dashboard.blade.php to get the user role with auth()->user()->getRole():
|
1 2 3 4 5 6 7 |
<Homepage :user-name='@json(auth()->user()->name)' :user-id='@json(auth()->user()->id)' :user-email='@json(auth()->user()->email)' :user-admin='@json(auth()->user()->hasRole("admin"))' :user-role='@json(auth()->user()->getRole())'> </Homepage> |
Then catch the props value in resources/js/components/Homepage.vue:
|
1 2 3 4 5 6 7 |
props: { ... userRole: { type: String, required: true } }, |
Let’s include some styles so that the page looks good. Below the closing template tag, paste the following code:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
<style scoped> @import url(https://fonts.googleapis.com/css?family=Dosis:300|Lato:300,400,600,700|Roboto+Condensed:300,700|Open+Sans+Condensed:300,600|Open+Sans:400,300,600,700|Maven+Pro:400,700); @import url("https://netdna.bootstrapcdn.com/font-awesome/4.2.0/css/font-awesome.css"); * { -moz-box-sizing: border-box; -webkit-box-sizing: border-box; box-sizing: border-box; } header { color: #d3d3d3; } nav { position: absolute; top: 0; bottom: 0; right: 82%; left: 0; padding: 22px; border-right: 2px solid #161e23; } nav > header { font-weight: 700; font-size: 0.8rem; text-transform: uppercase; } nav section { font-weight: 600; } nav section header { padding-top: 30px; } nav section ul { list-style: none; padding: 0px; } nav section ul a { color: white; text-decoration: none; font-weight: bold; } article { position: absolute; top: 0; bottom: 0; right: 0; left: 18%; overflow: auto; border-left: 2px solid #2a3843; padding: 20px; } article > header { height: 60px; border-bottom: 1px solid #2a3843; } </style> |
We are using the scoped attribute on the <style> tag because we want the CSS to only be applied on the Homepage component.
Next, let’s add the <script> section that will use the props we passed down from the parent component. We will also define the method that controls the log out feature here. Below the closing style tag, paste the following code:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
<script> export default { name: "Homepage", props: { userId: { type: Number, required: true }, userName: { type: String, required: true } }, data() { return {}; }, methods: { logout() { axios.post("/logout").then(() => { window.location = "/"; }); } } }; </script> |
SETTING UP THE READ VIEW
In the resources/js/app.js file, we defined the path of the read component as /admin/dashboard, which is the same address as the Homepage component. This will make sure the Read component always loads by default.
In the Read component, we want to load all of the available posts. We are also going to add Update and Delete options to each post. Clicking on these options will lead to the update and delete views respectively.
Open the Read.vue file and paste the following:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
<!-- File: ./resources/js/components/Read.vue --> <template> <div id="posts"> <p class="border p-3" v-for="post in posts"> {{ post.title }} <router-link :to="{ name: 'update', params: { postId : post.id } }"> <button type="button" class="p-1 mx-3 float-right btn btn-light"> Update </button> </router-link> <button type="button" @click="deletePost(post.id)" class="p-1 mx-3 float-right btn btn-danger" > Delete </button> </p> <div> <button v-if="next" type="button" @click="navigate(next)" class="m-3 btn btn-primary" > Next </button> <button v-if="prev" type="button" @click="navigate(prev)" class="m-3 btn btn-primary" > Previous </button> </div> </div> </template> |
Above, we have the template to handle the posts that are loaded from the API (Will get the data via API later). Next, paste the following below the closing template tag:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
<script> export default { mounted() { this.getPosts(); }, data() { return { posts: {}, next: null, prev: null }; }, methods: { getPosts(address) { axios.get(address ? address : "/api/posts").then(response => { this.posts = response.data.data; this.prev = response.data.links.prev; this.next = response.data.links.next; }); }, deletePost(id) { axios.delete("/api/posts/" + id).then(response => this.getPosts()) }, navigate(address) { this.getPosts(address) } } }; </script> |
In the script above, we defined a getPosts() method that requests a list of posts from the backend server. We also defined a posts object as a data property. This object will be populated whenever posts are received from the backend server.
We defined next and prev data string properties to store pagination links and only display the pagination options where it is available.
Lastly, we defined a deletePost() method that takes the id of a post as a parameter and sends a DELETE request to the API interface using Axios.
TESTING THE APPLICATION
Now that we have completed the first few components, we can serve the application using this command:
|
1 |
php artisan serve |
We will also build the assets so that our JavaScript is compiled for us. To do this, will run the command below in the root of the project folder:
|
1 |
npm run dev |
We can visit the application’s URL http://localhost:8000 and log in as an admin user. It’d show the admin dashboard but no post will show up because we haven’t set up the ‘api’ route yet.
NOTE: If you got an error like
|
1 |
Unknown custom element: <homepage> - did you register the component correctly? For recursive components, make sure to provide the "name" option. |
It because the app.js file not loaded. Probably you call it wrong. Make sure app.js in this path:
|
1 |
laravel-cms/resources/js/app.js |
NOT in laravel-cms/resources/assets/js/app.js
We will start building the API for the application. We will create an API for CRUD operations that an admin will perform on posts and we will test the endpoints using Postman.
BUILDING THE API USING LARAVEL’S API RESOURCES
The Laravel framework makes it very easy to build APIs. It has an API resources feature that we can easily adopt in our project. You can think of API resources as a transformation layer between Eloquent models and the JSON responses that will be sent back by our API.
ALLOWING MASS ASSIGNMENT ON SPECIFIED FIELDS
Since we are going to be performing CRUD operations on the posts in the application, we have to explicitly specify that it’s permitted for some fields to be mass-assigned data. For security reasons, Laravel prevents mass assignment of data to model fields by default.
Open the Post.php file and include this line of code:
|
1 2 |
// File: ./app/Post.php protected $fillable = ['user_id', 'title', 'body', 'image']; |
DEFINING API ROUTES
We will use the apiResource() method to generate only API routes. Open the routes/api.php file and add the following code:
|
1 2 |
// File: ./routes/api.php Route::apiResource('posts', 'PostController'); |
Because we will be handling the API requests on the /posts URL using the PostController, we will have to include some additional action methods in our post controller.
CREATING THE POST RESOURCE
At the beginning of this section, we already talked about what Laravel’s API resources are. Here, we create a resource class for our Post model. This will enable us to retrieve Post data and return formatted JSON format.
To create a resource class for our Post model run the following command in your terminal:
|
1 2 |
teddy@teddy:~/Documents/works/laravel/laravel-cms$ php artisan make:resource PostResource Resource created successfully. |
A new PostResource.php file will be available in the app/Http/Resources directory of our application. Open up the PostResource.php file and replace the toArray() method with the following:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// File: ./app/Http/Resources/PostResource.php public function toArray($request) { //return parent::toArray($request); return [ 'id' => $this->id, 'title' => $this->title, 'body' => $this->body, 'image' => $this->image, 'created_at' => (string) $this->created_at, 'updated_at' => (string) $this->updated_at, ]; } |
The job of this toArray() method is to convert our Post resource into an array. As seen above, we have specified the fields on our Post model, which we want to be returned as JSON when we make a request for posts.
We are also explicitly casting the dates, created_at and update_at, to strings so that they would be returned as date strings. The dates are normally an instance of Carbon.
Now that we have created a resource class for our Post model, we can start building the API’s action methods in our PostController and return instances of the PostResource where we want.
ADDING THE ACTION METHODS TO THE POST CONTROLLER
The usual actions performed on a post include the following:
Create – the process of creating a new post.
Read – the process of reading one or more posts.
Update – the process of updating an already published post.
Delete – the process of deleting a post.
In the last article, we already implemented a kind of ‘Read’ functionality when we defined the all and single methods. These methods allow users to browse through posts on the homepage.
In this section, we will define the methods that will resolve our API requests for creating, reading, updating and deleting posts.
The first thing we want to do is import the PostResource class at the top of the PostController.php file:
|
1 2 |
// File: ./app/Http/Controllers/PostController.php use App\Http\Resources\PostResource; |
Because we created the PostController as a resource controller, we already have the resource action methods included for us in the PostController.php file, we will be updating them with fitting snippets of code.
BUILDING THE HANDLER ACTION FOR THE CREATE OPERATION
In the PostController update the store() action method with the code snippet below. It will allow us to validate and create a new post:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
// File: ./app/Http/Controllers/PostController.php public function store(Request $request) { $this->validate($request, [ 'title' => 'required', 'body' => 'required', 'user_id' => 'required', 'image' => 'required|mimes:jpeg,png,jpg,gif,svg', ]); $post = new Post; if ($request->hasFile('image')) { $image = $request->file('image'); $name = str_slug($request->title).'.'.$image->getClientOriginalExtension(); $destinationPath = public_path('/uploads/posts'); $imagePath = $destinationPath . "/" . $name; $image->move($destinationPath, $name); $post->image = $name; } $post->user_id = $request->user_id; $post->title = $request->title; $post->body = $request->body; $post->save(); return new PostResource($post); } |
Here’s a breakdown of what this method does:
Receives a new request.
Validates the request.
Creates a new post.
Returns the post as a PostResource, which in turn returns a JSON formatted response.
BUILDING THE HANDLER ACTION FOR THE READ OPERATIONS
What we want here is to be able to read all the created posts or a single post. This is possible because the apiResource() method defines the API routes using standard REST rules.
This means that a GET request to this address, http://localhost:8000/api/posts, should be resolved by the index() action method. Let’s update the index method with the following code:
|
1 2 3 4 5 |
// File: ./app/Http/Controllers/PostController.php public function index() { return PostResource::collection(Post::latest()->paginate(5)); } |
This method will allow us to return a JSON formatted collection of all of the stored posts. We also want to paginate the response as this will allow us to create a better view on the admin dashboard.
Following the RESTful conventions as we discussed above, a GET request to this address, http://localhost:8000/api/posts/id, should be resolved by the show() action method. Let’s update the method with the fitting snippet:
|
1 2 3 4 5 |
// File: ./app/Http/Controllers/PostController.php public function show(Post $post) { return new PostResource($post); } |
Awesome, now this method will return a single instance of a post resource upon API query.
BUILDING THE HANDLER ACTION FOR THE UPDATE OPERATION
Next, let’s update the update() method in the PostController class. It will allow us to modify an existing post:
|
1 2 3 4 5 6 7 8 9 10 11 12 |
// File: ./app/Http/Controllers/PostController.php public function update(Request $request, Post $post) { $this->validate($request, [ 'title' => 'required', 'body' => 'required', ]); $post->update($request->only(['title', 'body'])); return new PostResource($post); } |
This method receives a request and a post id as parameters, then we use route model binding to resolve the id into an instance of a Post. First, we validate the $request attributes, then we update the title and body fields of the resolved post.
BUILDING THE HANDLER ACTION FOR THE DELETE OPERATION
Let’s update the destroy() method in the PostController class. This method will allow us to remove an existing post:
|
1 2 3 4 5 6 7 |
// File: ./app/Http/Controllers/PostController.php public function destroy(Post $post) { $post->delete(); return response()->json(null, 204); } |
In this method, we resolve the Post instance, then delete it and return a 204 response code.
Our methods are complete. We have a method to handle our CRUD operations.
We are not done with the dashboard just yet. In the next part, we will add the views that lets us create and update posts.
we have built the first parts of the admin dashboard using Vue. We also made it into an SPA with the VueRouter, this means that visiting the pages does not cause a reload to the web browser.
We only built the wrapper component and the Read component that retrieves the posts to be loaded so an admin can manage them.
we will build the view that will allow users to create and update posts. We will start writing code in the Update.vue and Create.vue files that we created in the previous article.
When we are done with this part, we will have additional functionalities like create and updating.
INCLUDING THE NEW ROUTES IN VUE ROUTER
In the previous article, we only defined the route for the Read component, we need to include the route configuration for the new components that we are about to build; Update and Create.
Open the resources/js/app.js file and replace the contents with the code below:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
import Homepage from './components/Homepage' import Create from './components/Create' import Read from './components/Read' import Update from './components/Update' Vue.use(VueRouter) const router = new VueRouter({ mode: 'history', routes: [ { path: '/admin/dashboard', name: 'read', component: Read, props: true }, { path: '/admin/create', name: 'create', component: Create, props: true }, { path: '/admin/update', name: 'update', component: Update, props: true }, ], }); |
Above, we have added two new components to the JavaScript file. We have the Create and Read components. We also added them to the router so that they can be loaded using the specified URLs.
BUILDING THE CREATE VIEW
Open the Create.vue file and update it with this markup template:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
<!-- File: ./resources/js/components/Create.vue --> <template> <div class="container"> <form> <div :class="['form-group m-1 p-3', (successful ? 'alert-success' : '')]"> <span v-if="successful" class="label label-sucess">Published!</span> </div> <div :class="['form-group m-1 p-3', error ? 'alert-danger' : '']"> <span v-if="errors.title" class="label label-danger"> {{ errors.title[0] }} </span> <span v-if="errors.body" class="label label-danger"> {{ errors.body[0] }} </span> <span v-if="errors.image" class="label label-danger"> {{ errors.image[0] }} </span> </div> <div class="form-group"> <input type="title" ref="title" class="form-control" id="title" placeholder="Enter title" required> </div> <div class="form-group"> <textarea class="form-control" ref="body" id="body" placeholder="Enter a body" rows="8" required></textarea> </div> <div class="custom-file mb-3"> <input type="file" ref="image" name="image" class="custom-file-input" id="image" required> <label class="custom-file-label" >Choose file...</label> </div> <button type="submit" @click.prevent="create" class="btn btn-primary block"> Submit </button> </form> </div> </template> |
Above we have the template for the Create component. If there is an error during post creation, there will be a field indicating the specific error. When a post is successfully published, there will also a message saying it was successful.
Let’s include the script logic that will perform the sending of posts to our backend server and read back the response.
After the closing template tag add this:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
<script> export default { props: { userId: { type: Number, required: true } }, data() { return { error: false, successful: false, errors: [] }; }, methods: { create() { const formData = new FormData(); formData.append("title", this.$refs.title.value); formData.append("body", this.$refs.body.value); formData.append("user_id", this.userId); formData.append("image", this.$refs.image.files[0]); axios .post("/api/posts", formData) .then(response => { this.successful = true; this.error = false; this.errors = []; }) .catch(error => { if (!_.isEmpty(error.response)) { if ((error.response.status = 422)) { this.errors = error.response.data.errors; this.successful = false; this.error = true; } } }); this.$refs.title.value = ""; this.$refs.body.value = ""; } } }; </script> |
In the script above, we defined a create() method that takes the values of the input fields and uses the Axios library to send them to the API interface on the backend server. Within this method, we also update the status of the operation, so that an admin user can know when a post is created successfully or not.
BUILDING THE UPDATE VIEW
Let’s start building the Update component. Open the Update.vue file and update it with this markup template:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
<!-- File: ./resources/js/components/Update.vue --> <template> <div class="container"> <form> <div :class="['form-group m-1 p-3', successful ? 'alert-success' : '']"> <span v-if="successful" class="label label-sucess">Updated!</span> </div> <div :class="['form-group m-1 p-3', error ? 'alert-danger' : '']"> <span v-if="errors.title" class="label label-danger"> {{ errors.title[0] }} </span> <span v-if="errors.body" class="label label-danger"> {{ errors.body[0] }} </span> </div> <div class="form-group"> <input type="title" ref="title" class="form-control" id="title" placeholder="Enter title" required> </div> <div class="form-group"> <textarea class="form-control" ref="body" id="body" placeholder="Enter a body" rows="8" required></textarea> </div> <button type="submit" @click.prevent="update" class="btn btn-primary block"> Submit </button> </form> </div> </template> |
This template is similar to the one in the Create component. Let’s add the script for the component.
Below the closing template tag, paste the following:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
<script> export default { mounted() { this.getPost(); }, props: { postId: { type: Number, required: true } }, data() { return { error: false, successful: false, errors: [] }; }, methods: { update() { let title = this.$refs.title.value; let body = this.$refs.body.value; axios .put("/api/posts/" + this.postId, { title, body }) .then(response => { this.successful = true; this.error = false; this.errors = []; }) .catch(error => { if (!_.isEmpty(error.response)) { if ((error.response.status = 422)) { this.errors = error.response.data.errors; this.successful = false; this.error = true; } } }); }, getPost() { axios.get("/api/posts/" + this.postId).then(response => { this.$refs.title.value = response.data.data.title; this.$refs.body.value = response.data.data.body; }); } } }; </script> |
In the script above, we make a call to the getPosts() method as soon as the component is mounted. The getPosts() method fetches the data of a single post from the backend server, using the postId.
When Axios sends back the data for the post, we update the input fields in this component so they can be updated.
Finally, the update() method takes the values of the fields in the components and attempts to send them to the backend server for an update. In a situation where the fails, we get instant feedback.
TESTING THE APPLICATION
To test that our changes work, we want to refresh the database and restore it back to a fresh state. To do this, run the following command in your terminal:
|
1 |
$ php artisan migrate:fresh --seed |
Next, let’s compile our JavaScript files and assets. This will make sure all the changes we made in the Vue component and the app.js file gets built. To recompile, run the command below in your terminal:
|
1 |
$ npm run dev |
Lastly, we need to serve the application. To do this, run the following command in your terminal window:
|
1 |
$ php artisan serve |
If you had the serve command running before, then you might need to restart it.
We will visit the application’s http://localhost:8000 and log in as an admin user. From the dashboard, you can test the create and update feature
Now, we will be adding support for comments. We will also ensure that the comments on each post are updated in realtime, so a user doesn’t have to refresh the page to see new comments.
ADDING COMMENTS TO THE BACKEND
When we were creating the API, we did not add the support for comments to the post resource, so we will have to do so now. Open the API project in your text editor as we will be modifying the project a little.
The first thing we want to do is create a model, controller, and a migration for the comment resource. To do this, open your terminal and cd to the project directory and run the following command:
|
1 2 3 4 |
teddy@teddy:~/Documents/works/laravel/laravel-cms$ php artisan make:model Comment -mc Model created successfully. Created Migration: 2019_01_30_042953_create_comments_table Controller created successfully. |
The command above will create a model called Comment, a controller called CommentController, and a migration file in the database/migrations directory.
UPDATING THE COMMENTS MIGRATION FILE
To update the comments migration navigate to the database/migrations folder and find the newly created migration file for the Comment model. Let’s update the up() method in the file:
|
1 2 3 4 5 6 7 8 9 10 11 |
//database/migrations/2019_01_30_042953_create_comments_table.php public function up() { Schema::create('comments', function (Blueprint $table) { $table->increments('id'); $table->timestamps(); $table->integer('user_id')->unsigned(); $table->integer('post_id')->unsigned(); $table->text('body'); }); } |
We included user_id and post_id fields because we intend to create a link between the comments, users, and posts. The body field will contain the actual comment.
DEFINING THE RELATIONSHIPS AMONG THE COMMENT, USER, AND POST MODELS
In this application, a comment will belong to a user and a post because a user can make a comment on a specific post, so we need to define the relationship that ties everything up.
Open the User model and include this method:
|
1 2 3 4 5 |
// File: ./app/User.php public function comments() { return $this->hasMany(Comment::class); } |
This is a relationship that simply says that a user can have many comments. Now let’s define the same relationship on the Post model. Open the Post.php file and include this method:
|
1 2 3 4 5 |
// File: ./app/Post.php public function comments() { return $this->hasMany(Comment::class); } |
Finally, we will include two methods in the Comment model to complete the second half of the relationships we defined in the User and Post models.
Open the app/Comment.php file and include these methods:
|
1 2 3 4 5 6 7 8 9 10 |
// File: ./app/Comment.php public function user() { return $this->belongsTo(User::class); } public function post() { return $this->belongsTo(Post::class); } |
Since we want to be able to mass assign data to specific fields of a comment instance during comment creation, we will include this array of permitted assignments in the app/Comment.php file:
|
1 |
protected $fillable = ['user_id', 'post_id', 'body']; |
We can now run our database migration for our comments:
|
1 2 3 |
teddy@teddy:~/Documents/works/laravel/laravel-cms$ php artisan migrate Migrating: 2019_01_30_042953_create_comments_table Migrated: 2019_01_30_042953_create_comments_table |
CONFIGURING LARAVEL TO BROADCAST EVENTS USING PUSHER
We already said that the comments will have a realtime functionality and we will be building this using Pusher, so we need to enable Laravel’s event broadcasting feature.
Open the config/app.php file and uncomment the following line in the providers array:
|
1 |
App\Providers\BroadcastServiceProvider, |
Next, we need to configure the broadcast driver in the .env file. Replace from BROADCAST_DRIVER=log TO
|
1 |
BROADCAST_DRIVER=pusher |
Let’s pull in the Pusher PHP SDK using composer:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
teddy@teddy:~/Documents/works/laravel/laravel-cms$ composer require pusher/pusher-php-server Using version ^3.3 for pusher/pusher-php-server ./composer.json has been updated Loading composer repositories with package information Updating dependencies (including require-dev) Package operations: 2 installs, 0 updates, 0 removals - Installing paragonie/sodium_compat (v1.8.1): Downloading (100%) - Installing pusher/pusher-php-server (v3.3.1): Downloading (100%) paragonie/sodium_compat suggests installing ext-libsodium (PHP < 7.0: Better performance, password hashing (Argon2i), secure memory management (memzero), and better security.) Writing lock file Generating optimized autoload files > Illuminate\Foundation\ComposerScripts::postAutoloadDump > @php artisan package:discover --ansi Discovered Package: beyondcode/laravel-dump-server Discovered Package: fideloper/proxy Discovered Package: laravel/nexmo-notification-channel Discovered Package: laravel/slack-notification-channel Discovered Package: laravel/tinker Discovered Package: nesbot/carbon Discovered Package: nunomaduro/collision Package manifest generated successfully. |
CONFIGURING PUSHER
For us to use Pusher in this application, it is a prerequisite that you have a Pusher account. You can create a free Pusher account here then login to your dashboard and create an app.
Once you have created an app, we will use the app details to configure pusher in the .env file:
|
1 2 3 4 |
PUSHER_APP_ID=xxxxxx PUSHER_APP_KEY=xxxxxxxxxxxxxxxxxxxx PUSHER_APP_SECRET=xxxxxxxxxxxxxxxxxxxx PUSHER_APP_CLUSTER=xx |
Update the Pusher keys with the app credentials provided for you under the Keys section on the Overview tab on the Pusher dashboard.
BROADCASTING AN EVENT FOR WHEN A NEW COMMENT IS SENT
To make the comment update realtime, we have to broadcast an event based on the comment creation activity. We will create a new event and call it CommentSent. It is to be fired when there is a successful creation of a new comment.
Run command in your terminal:
|
1 2 |
teddy@teddy:~/Documents/works/laravel/laravel-cms$ php artisan make:event CommentSent Event created successfully. |
There will be a newly created file in the app\Events directory, open the CommentSent.php file and ensure that it implements the ShouldBroadcast interface.
Open and replace the file with the following code:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
// File: ./app/Events/CommentSent.php <?php namespace App\Events; use App\Comment; use App\User; use Illuminate\Broadcasting\Channel; use Illuminate\Queue\SerializesModels; use Illuminate\Broadcasting\PrivateChannel; use Illuminate\Broadcasting\PresenceChannel; use Illuminate\Foundation\Events\Dispatchable; use Illuminate\Broadcasting\InteractsWithSockets; use Illuminate\Contracts\Broadcasting\ShouldBroadcast; class CommentSent implements ShouldBroadcast { use Dispatchable, InteractsWithSockets, SerializesModels; public $user; public $comment; public function __construct(User $user, Comment $comment) { $this->user = $user; $this->comment = $comment; } public function broadcastOn() { return new PrivateChannel('comment'); } } |
In the code above, we created two public properties, user and comment, to hold the data that will be passed to the channel we are broadcasting on. We also created a private channel called comment. We are using a private channel so that only authenticated clients can subscribe to the channel.
NOTE: Make sure ‘CommentSent’ class implements ‘ShouldBroadcast’ class. When I accidentally missing to implement it, the message can’t be broadcasted at real time properly. I need to refresh the page to see the incoming message on the another login.
DEFINING THE ROUTES FOR HANDLING OPERATIONS ON A COMMENT
We created a controller for the comment model earlier but we haven’t defined the web routes that will redirect requests to be handled by that controller.
Open the routes/web.php file and include the code below:
|
1 2 3 |
// File: ./routes/web.php Route::get('/{post}/comments', 'CommentController@index'); Route::post('/{post}/comments', 'CommentController@store'); |
SETTING UP THE ACTION METHODS IN THE COMMENTCONTROLLER
We need to include two methods in the CommentController.php file, these methods will be responsible for storing and retrieving methods. In the store() method, we will also be broadcasting an event when a new comment is created.
Open the CommentController.php file and replace its contents with the code below:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
// File: ./app/Http/Controllers/CommentController.php <?php namespace App\Http\Controllers; use App\Comment; use App\Events\CommentSent; use App\Post; use Illuminate\Http\Request; class CommentController extends Controller { public function store(Post $post) { $this->validate(request(), [ 'body' => 'required', ]); $user = auth()->user(); $comment = Comment::create([ 'user_id' => $user->id, 'post_id' => $post->id, 'body' => request('body'), ]); broadcast(new CommentSent($user, $comment))->toOthers(); return ['status' => 'Message Sent!']; } public function index(Post $post) { return $post->comments()->with('user')->get(); } } |
In the store method above, we are validating then creating a new post comment. After the comment has been created, we broadcast the CommentSent event to other clients so they can update their comments list in realtime.
In the index method we just return the comments belonging to a post along with the user that made the comment.
ADDING A LAYER OF AUTHENTICATION
Let’s add a layer of authentication that ensures that only authenticated users can listen on the private comment channel we created.
Add the following code to the routes/channels.php file:
|
1 2 3 4 |
// File: ./routes/channels.php Broadcast::channel('comment', function ($user) { return auth()->check(); }); |
ADDING COMMENTS TO THE FRONTEND
In the second article of this series, we created the view for the single post landing page in the single.blade.php file, but we didn’t add the comments functionality. We are going to add it now. We will be using Vue to build the comments for this application so the first thing we will do is to include Vue in the frontend of our application.
Open the master layout template and include Vue to its <head> tag. Just before the <title> tag appears in the master.blade.php file, include this snippet:
|
1 2 3 |
<!-- File: ./resources/views/layouts/master.blade.php --> <meta name="csrf-token" content="{{ csrf_token() }}"> <script src="{{ asset('js/app.js') }}" defer></script> |
The csrf_token() is there so that users cannot forge requests in our application. All our requests will pick the randomly generated csrf-token and use that to make requests.
Related: CSRF in Laravel: how VerifyCsrfToken works and how to prevent attacks
Now the next thing we want to do is update the resources/js/app.js file so that it includes a template for the comments view.
Open the file and replace its contents with the code below:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
//resources/js/app.js require('./bootstrap'); import Vue from 'vue' ... import Comments from './components/Comments' Vue.use(VueRouter) const router = new VueRouter({ ... }); const app = new Vue({ el: '#app', components: { Homepage, Comments }, router, }); |
Above we imported the Comment component and then we added it to the list of components in the applications Vue instance.
Now create a Comments.vue file in the resources/js/components directory. This is where all the code for our comment view will go. We will populate this file later on.
INSTALLING PUSHER AND LARAVEL ECHO
For us to be able to use Pusher and subscribe to events on the frontend, we need to pull in both Pusher and Laravel Echo. We will do so by running this command:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
teddy@teddy:~/Documents/works/laravel/laravel-cms$ npm install --save laravel-echo pusher-js npm WARN rm not removing /home/teddy/Documents/works/laravel/laravel-cms/node_modules/.bin/regjsparser as it wasn't installed by /home/teddy/Documents/works/laravel/laravel-cms/node_modules/regjsparser npm WARN rm not removing /home/teddy/Documents/works/laravel/laravel-cms/node_modules/.bin/json5 as it wasn't installed by /home/teddy/Documents/works/laravel/laravel-cms/node_modules/json5 npm WARN rm not removing /home/teddy/Documents/works/laravel/laravel-cms/node_modules/.bin/jsesc as it wasn't installed by /home/teddy/Documents/works/laravel/laravel-cms/node_modules/jsesc npm WARN rm not removing /home/teddy/Documents/works/laravel/laravel-cms/node_modules/.bin/cssesc as it wasn't installed by /home/teddy/Documents/works/laravel/laravel-cms/node_modules/cssesc > webpack-cli@3.2.1 postinstall /home/teddy/Documents/works/laravel/laravel-cms/node_modules/webpack-cli > lightercollective *** Thank you for using webpack-cli! *** Please consider donating to our open collective to help us maintain this package. https://opencollective.com/webpack/donate *** npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.7 (node_modules/fsevents): npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.7: wanted {"os":"darwin","arch":"any"} (current: {"os":"linux","arch":"x64"}) + laravel-echo@1.5.2 + pusher-js@4.3.1 added 87 packages from 71 contributors, removed 51 packages, updated 852 packages and audited 16870 packages in 12.86s found 0 vulnerabilities |
Laravel Echo is a JavaScript library that makes it easy to subscribe to channels and listen for events broadcast by Laravel.
Now let’s configure Laravel Echo to work in our application. In the resources/js/bootstrap.js file, find and uncomment this snippet of code:
|
1 2 3 4 5 6 7 8 9 10 11 |
//resources/js/bootstrap.js import Echo from 'laravel-echo' window.Pusher = require('pusher-js'); window.Echo = new Echo({ broadcaster: 'pusher', key: process.env.MIX_PUSHER_APP_KEY, cluster: process.env.MIX_PUSHER_APP_CLUSTER, encrypted: true }); |
The key and cluster will pull the keys from your .env file so no need to enter them manually again.
Now let’s import the Comments component into the single.blade.php file and pass along the required the props.
Open the single.blade.php file and replace its contents with the code below:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
{{-- File: ./resources/views/single.blade.php --}} @extends('layouts.master') @section('content') <div class="container"> <div class="row"> <div class="col-lg-10 mx-auto"> <h3 class="mt-4">{{ $post->title }} <span class="lead"> by <a href="#"> {{ $post->user->name }} </a></span> </h3> <hr> <p>Posted {{ $post->created_at->diffForHumans() }} </p> <hr> <img class="img-fluid rounded" src=" {!! !empty($post->image) ? '/uploads/posts/' . $post->image : 'http://placehold.it/750x300' !!} " alt=""> <hr> <div> <p>{{ $post->body }}</p> <hr> <br> </div> <!--<div class="card my-4"> <h5 class="card-header">Leave a Comment:</h5> <div class="card-body"> <form> <div class="form-group"> <textarea class="form-control" rows="3"></textarea> </div> <button type="submit" class="btn btn-primary">Submit</button> </form> </div> </div>--> @auth <Comments :post-id = '@json($post->id)' :user-name = '@json(auth()->user()->name)'> </Comments> @endauth </div> </div> </div> @endsection |
BUILDING THE COMMENTS VIEW
Open the Comments.vue file and add the following markup template below:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
<!-- File: ./resources/js/components/Comments.vue --> <template> <div class="card my-4"> <h5 class="card-header">Leave a Comment:</h5> <div class="card-body"> <form> <div class="form-group"> <textarea ref="body" class="form-control" rows="3"></textarea> </div> <button type="submit" @click.prevent="addComment" class="btn btn-primary"> Submit </button> </form> </div> <p class="border p-3" v-for="comment in comments"> <strong>{{ comment.user.name }}</strong>: <span>{{ comment.body }}</span> </p> </div> </template> |
Now, we’ll add a script that defines two methods:
fetchComments() – this will fetch all the existing comments when the component is created.
addComment() – this will add a new comment by hitting the backend server. It will also trigger a new event that will be broadcast so all clients receive them in realtime.
In the same file, add the following below the closing template tag:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
<!-- File: ./resources/js/components/Comments.vue --> <template> ... </template> <script> export default { props: { userName: { type: String, required: true }, postId: { type: Number, required: true } }, data() { return { comments: [] }; }, created() { this.fetchComments(); }, methods: { fetchComments() { axios.get("/" + this.postId + "/comments").then(response => { this.comments = response.data; }); }, addComment() { let body = this.$refs.body.value; axios.post("/" + this.postId + "/comments", { body }).then(response => { this.comments.push({ user: {name: this.userName}, body: this.$refs.body.value }); this.$refs.body.value = ''; }); } }, } </script> |
In the created() method above, we first made a call to the fetchComments() method, then we created a listener to the private comment channel using Laravel Echo. Once this listener is triggered, the comments property is updated.
TESTING THE APPLICATION
Now let’s test the application to see if it is working as intended. Before running the application, we need to refresh our database so as to revert any changes. To do this, run the command below in your terminal:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
teddy@teddy:~/Documents/works/laravel/laravel-cms$ php artisan migrate:fresh --seed Dropped all tables successfully. Migration table created successfully. Migrating: 2014_10_12_000000_create_users_table Migrated: 2014_10_12_000000_create_users_table Migrating: 2014_10_12_100000_create_password_resets_table Migrated: 2014_10_12_100000_create_password_resets_table Migrating: 2019_01_21_031440_create_roles_table Migrated: 2019_01_21_031440_create_roles_table Migrating: 2019_01_21_032646_create_role_user_table Migrated: 2019_01_21_032646_create_role_user_table Migrating: 2019_01_21_083457_create_posts_table Migrated: 2019_01_21_083457_create_posts_table Migrating: 2019_01_30_042953_create_comments_table Migrated: 2019_01_30_042953_create_comments_table Seeding: RoleTableSeeder Seeding: UserTableSeeder Seeding: PostTableSeeder Database seeding completed successfully. |
Next, let’s build the application so that all the changes will be compiled and included as a part of the JavaScript file. To do this, run the following command on your terminal:
|
1 |
npm run dev |
Finally, let’s serve the application using this command:
|
1 |
php artisan serve |
To test that our application works visit the application URL http://localhost:8000 on two separate browser windows, we will log in to our application on each of the windows as a different user.
We will finally make a comment on the same post on each of the browser windows and check that it updates in realtime on the other window:
IMPROVEMENTS:
Currently the comments can only be displayed if the user is logged in. But I want to display them also if no user is logged in but it only read the comments and can’t add the comment. So modify resources/views/single.blade.php:
|
1 2 3 4 5 6 7 8 9 10 11 |
@auth <Comments :post-id='@json($post->id)' :user-name='@json(auth()->user()->name)'> </Comments> @else <Comments :post-id='@json($post->id)' :user-name='@json("")'> </Comments> @endauth |
Make user-name variable is empty string. Then modify resources/js/components/Comments.vue on the template and the script:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
<template> <div class="card my-4"> <h5 class="card-header" v-if="userName"> Leave a comment: </h5> <h5 class="card-header" v-else> Comments: (<a href="/login">Login</a> to add comment) </h5> <div class="card-body" v-if="userName"> ... </div> <p class="border p-3" v-for="comment in comments"> ... </p> </div> </template> |
On the template, show the text input if ‘userName’ is exist. Otherwise just show the login link. Then on the script, get the broadcast only if the ‘userName’ is exist:
|
1 2 3 4 5 6 7 8 9 |
created() { this.fetchComments(); if(this.userName){ Echo.private("comment").listen("CommentSent", e => { ... }); } }, |
Anyway if the user not logged in, they can’t get the real time message!
How to show the message counter?
Modify the template and the script in resources/js/components/Comments.vue. On the script, add a new computed variable ‘commentCount’ in ‘data’. Why not in ‘props’? because the variables in ‘data’ can be changed real time. not only on the page load.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
data() { return { comments: [], commentCount: 0 }; }, created() { ... if(this.userName){ Echo.private("comment").listen("CommentSent", e => { this.comments.push({ ... }); this.commentCount = this.comments.length; }); } }, methods: { fetchComments() { axios.get("/" + this.postId + "/comments").then(response => { this.comments = response.data; this.commentCount = response.data.length; }); }, addComment() { ... axios.post("/" + this.postId + "/comments", { body }).then(response => { this.comments.push({ ... }); this.commentCount = this.comments.length; this.$refs.body.value = ''; }); } }, |
‘commentCount’ is calculated in ‘addComment’ method when the new comment is inserted. Also in ‘fetchComments’ when the page is loaded. To calculate it at real time, also put it in ‘created’. Then show the counter on the template:
|
1 2 3 4 5 6 |
<h5 class="card-header" v-if="userName"> There are {{ commentCount }} comments. Leave a comment: </h5> <h5 class="card-header" v-else> There are {{ commentCount }} comments. <a href="/login">Login</a> to add comment. </h5> |
So the message counter would appear on the post page (single page). But how to show the counter on the homepage?
So modify resources/views/landing.blade.php to show the counter:
|
1 2 3 4 5 6 7 |
... <div class="card-body"> ... <a href="/posts/{{ $post->id }}" class="btn btn-primary">Read More →</a> <a href="/posts/{{ $post->id }}#comments" class="btn btn-primary">Comments ({{ $post->comments()->count() }})</a> </div> ... |
give the bookmark link (#) to jump straight to the comments section. Modify resources/js/components/Comments.vue to add the bookmark ‘comments’:
|
1 2 3 |
<template> <div id="comments" class="card my-4"> ... |