How to build Laravel packages
You will need Laravel 5.8+ installed on your machine.
In this tutorial, we will learn how to build a Laravel package with Vue.js and Pusher. The sample package we will be building will be a review system that can be added to a page you are building independently.
This package will ensure that the reviews on each page are updated in realtime so that visitors don’t have to refresh the page to see new reviews.
When we are done, our app will look like this:
Prerequisites
To follow along with this article, you need the following:
- A Pusher account. Sign up here if you don’t have one.
- Laravel installed on your machine. We are working with Laravel 5.8.
- Basic knowledge of the Laravel framework.
- Basic knowledge of JavaScript.
- Basic knowledge of Vue.js.
Let’s get started.
Setup the Laravel project
The first thing we need to do is create a Laravel project. We will use the Laravel installer to make this possible. Open your terminal, and run this command:
$ laravel new sampleLaravelApp
This will create a Laravel application named sampleLaravelApp
. Next, we will install a Composer package called laravel-packager. We need this to build our Laravel package. In the root directory of the Laravel project run this command:
$ composer require jeroen-g/laravel-packager --dev
Now we have installed the package. We can use it to scaffold our own package. Run this command in your projects directory:
$ php artisan packager:new Acme PageReview --i
The package will be created interactively. It will interactively ask some questions and your responses will be used to create a composer.json
file for our package.
Now you should have a folder named packages
inside your Laravel project. Inside the packages
folder, you will find a directory with your vendor name (Acme in our case), a subfolder representing the package name (PageReview).
Building the Laravel package
In this section, we will start customizing the generated package files with the actual code and add some new files where necessary. Before adding logic, note that when we were setting up the new Laravel package, laravel-packager
enabled Package Discovery for us.
This is a Laravel feature that automatically loads a package’s service provider. In your package’s composer.json
file, this is the snippet that enables it:
// File: ./packages/Acme/PageReview/composer.json
"extra": {
"laravel": {
"providers": [
"Acme\\PageReview\\PageReviewServiceProvider"
],
"aliases": {
"PageReview": "Acme\\review\\Facades\\PageReview"
}
}
}
Adding our database migration file
Next, let’s create a migration file to help us save reviews to the database. Create a new folder named database
in the packages/Acme/PageReview
directory. Inside this folder, create a migrations
folder.
Run this command in the Laravel project root directory to create the package migration:
$ php artisan make:migration create_page_reviews_tables --path=packages/Acme/PageReview/database/migrations
Update the up()
and down()
method of the migration file as seen below:
// File: packages/Acme/PageReview/database/migrations/*_create_page_reviews_table.php
public function up()
{
Schema::create('pages', function (Blueprint $table) {
$table->increments('id');
$table->text('path');
$table->timestamps();
});
Schema::create('reviews', function (Blueprint $table) {
$table->increments('id');
$table->text('page_id');
$table->text('username');
$table->text('comment');
$table->timestamps();
});
}
public function down()
{
Schema::dropIfExists('pages');
Schema::dropIfExists('reviews');
}
It is not recommended to create one migration for two tables but for brevity we will. However, in a real application, create one migration per database operation.
In the up
method, we have create two tables, the pages
and reviews
table. The pages
table will save the URL path
visited in our app. The reviews
table will contain the username
and comment
field for the actual review. We included page_id
field because we intend to create a link between the reviews and pages. The down
method will drop both tables when triggered.
Adding the package model files
In our packages/Acme/PageReview
directory open the src
directory and create a new folder called Models
. Inside the folder, create two new files called Review.php
and Page.php
.
Open the Review.php
and paste this snippet:
<?php
// File: packages/Acme/PageReview/src/Models/Review.php
namespace Acme\PageReview\Models;
use Illuminate\Database\Eloquent\Model;
class Review extends Model
{
protected $fillable = [
'page_id',
'username',
'comment'
];
}
Next, open the Page.php
and paste this snippet:
<?php
// File: packages/Acme/PageReview/src/Models/Page.php
namespace Acme\PageReview\Models;
use Illuminate\Database\Eloquent\Model;
class Page extends Model
{
protected $fillable = [
'path',
];
public function reviews()
{
return $this->hasMany(Review::class);
}
}
In the code above, the Page
model has a reviews
method, which forms a hasMany
relationship with the Review
model. This simply means a page can have many reviews.
Updating the package config file
We already have a config
folder inside our package directory. Inside this config
folder there is a single file representing our package configurations, where anyone using this package can easily override the default configuration options. The name of the file is pagereview.php
.
Update the pagereview.php
file like so:
<?php
// File: packages/Acme/PageReview/config/pagereview.php
return [
/*
* This determines how the reviews will be ordered when fetched
*/
'order' => [
'by' => 'DESC',
'as' => 'created_at',
],
];
Setting the package controller methods for certain actions
Let’s create a new folder Controllers
inside the packages/Acme/PageReview/src
directory. Inside the folder, create a file called PageReviewController.php
and paste this snippet there:
<?php
// File: packages/Acme/PageReview/src/Controllers/PageReviewController.php
namespace Acme\PageReview\Controllers;
use Illuminate\Http\Request;
use Acme\PageReview\Models\Page;
use Illuminate\Routing\Controller;
use Pusher\Laravel\Facades\Pusher;
class PageReviewController extends Controller
{
public function index(Request $request)
{
if (isset($request->path)) {
$page = Page::firstorCreate(['path' => $request->path]);
$reviews = $page->reviews()
->orderBy(
config('pagereview.order.as'),
config('pagereview.order.by')
)
->get();
return response()->json([
'page' => $page,
'reviews' => $reviews
]);
}
return response()->json([]);
}
public function store(Request $request)
{
$page = Page::firstorCreate(['path' => $request->path]);
$review = $page->reviews()->create([
'username' => $request->username,
'comment' => $request->comment,
]);
Pusher::trigger('page-'.$page->id, 'new-review', $review);
return $review;
}
}
This controller file has two methods:
index
: here we get a path and save it if it is not already saved, then we get the related reviews to that page and return both the page and reviews as a JSON response.store
: here we simply save the page, and review. We also trigger a message to the Pusher API so that channels listening can pick up the review.
Next, let’s add the Pusher package as a dependency. Open the composer.json
in the package and add it like so:
// File: packages/Acme/PageReview/composer.json
"require": {
// ...
"pusher/pusher-http-laravel": "^4.2"
},
Defining routes
Next, we will create a routes folder in the packages/Acme/PageReview
directory to save our routes. After creating the folder, create a web.php
file inside the folder and update it with the snippet below:
<?php
// File: packages/Acme/PageReview/routes/web.php
Route::get('pagereview', 'PageReviewController@index')->name('pagereview.index');
Route::post('pagereview', 'PageReviewController@store')->name('pagereview.store');
Updating the service provider
Most Laravel packages come with migrations, routes, config files or views. To be able to use these resources we have to load them in our package’s service provider.
You can read more about that in the official documentation
Update the PageReviewServiceProvider
file like so:
<?php
// packages/Acme/PageReview/src/PageReviewServiceProvider.php
namespace Acme\PageReview;
use Illuminate\Support\ServiceProvider;
class PageReviewServiceProvider extends ServiceProvider
{
/**
* Perform post-registration booting of services.
*
* @return void
*/
public function boot()
{
$this->loadMigrationsFrom(__DIR__ . '/../database/migrations');
$this->loadViewsFrom(__DIR__.'/../resources/views', 'pagereview');
$this->app['router']->namespace('Acme\\PageReview\\Controllers')
->middleware(['web'])
->group(function () {
$this->loadRoutesFrom(__DIR__ . '/../routes/web.php');
});
if ($this->app->runningInConsole()) {
$this->bootForConsole();
}
}
/**
* Register any package services.
*
* @return void
*/
public function register()
{
$this->mergeConfigFrom(__DIR__.'/../config/pagereview.php', 'pagereview');
}
/**
* Get the services provided by the provider.
*
* @return array
*/
public function provides()
{
return ['pagereview'];
}
/**
* Console-specific booting.
*
* @return void
*/
protected function bootForConsole()
{
$this->publishes([
__DIR__.'/../config/pagereview.php' => config_path('pagereview.php'),
], 'pagereview.config');
$this->publishes([
__DIR__.'/../resources/views' => base_path('resources/views/vendor/acme'),
], 'pagereview.views');
$this->publishes([
__DIR__ . '/../database/migrations/' => database_path('migrations'),
], 'migrations');
}
}
The boot
method loads resources such as migration, routes, or any other piece of functionality while the register
method only binds logic to the service container. We also have a bootForConsole
method meant to publish resource files such as views and migrations if it is loaded via the console.
Building the frontend
Now, we will create the frontend view for the package’s review section. It will be a simple UI that displays a form to add a review and also display a list of reviews with the author name.
Create a resources/views
folder in the packages/Acme/PageReview
directory and create a file called section.blade.php
. Paste this snippet to the file:
<!-- File: packages/Acme/PageReview/resources/views/section.blade.php -->
<div class="container" id="review">
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card">
<div class="card-body">
<h4>Add review</h4>
<div class="form-group">
<input type="text" v-model="username" class="form-control col-4" placeholder="Enter your username">
</div>
<div class="form-group">
<textarea class="form-control" v-model="comment" placeholder="Enter your review"></textarea>
</div>
<div class="form-group">
<input type="button" v-on:click="addPageReview" v-bind:disabled="isDisabled" class="btn btn-success" value="Add Review" />
</div>
<hr />
<h4>Display reviews</h4>
<div v-for="review in reviews">
<strong>@{{ review.username }}</strong>
<p>@{{ review.comment }}</p>
</div>
</div>
</div>
</div>
</div>
</div>
In the code above we added a few attributes to some elements:
- Both input fields have a
v-model
Vue directive allowing us to create a two-way binding. - The Add Review button has a
v-on:click
to intercept any DOM event by and triggers theaddPageReview
method on the Vue instance. - The
v-for
attribute enables us to iterate over the reviews data and display each review belonging to that page.
Still in the same file, add the following below the closing div
tag:
<!-- File: packages/vendor-name/PageReview/resources/views/section.blade.php -->
<!-- Add Vue Code -->
<script>
Pusher.logToConsole = true;
var review = new Vue({
el: '#review',
data: {
username: null,
comment: null,
path: window.location.pathname,
isDisabled: false,
reviews: [],
page: [],
},
methods: {
subscribe() {
var pusher = new Pusher('{{ env('PUSHER_APP_KEY')}}', {
cluster: '{{ env('PUSHER_APP_CLUSTER') }}',
});
pusher.subscribe('page-' + this.page.id)
.bind('new-review', this.fetchPageReviews);
},
fetchPageReviews() {
var vm = this;
var url = '{{ route('pagereview.index') }}' + '?path=' + this.path;
fetch(url)
.then(function(response) {
return response.json()
})
.then(function(json) {
vm.page = json.page
vm.reviews = json.reviews
vm.subscribe();
})
},
addPageReview(event) {
event.preventDefault();
this.isDisabled = true;
const token = document.head.querySelector('meta[name="csrf-token"]');
const data = {
path: this.path,
comment: this.comment,
username: this.username,
};
fetch('{{ route('pagereview.store') }}', {
body: JSON.stringify(data),
credentials: 'same-origin',
headers: {
'content-type': 'application/json',
'x-csrf-token': token.content,
},
method: 'POST',
mode: 'cors',
}).then(response => {
this.isDisabled = false;
if (response.ok) {
this.username = '';
this.comment = '';
this.fetchPageReviews();
}
})
},
},
created() {
this.fetchPageReviews();
}
});
</script>
Here, we have methods to fetch reviews and add reviews on a page. Our Vue instance is instantiated on the review
ID and has some data
values.
We also defined three methods:
subscribe
- this subscribes to anew-review
event on a Pusher channel on the current page for realtime update.fetchPageReviews
- this method will fetch all the existing reviews for this page when the loaded and also calls thesubscribe
method.addPageReview
- this will collect the input field values of username and text to as a review, thereby triggering an event broadcast on the backend, so all clients will receive in real time.
Finalizing and testing the package
Here, we will test the package we have just built. Inside the main Laravel app composer.json
file we need to add a new repository definition like this:
// [...]
"repositories": [
{
"type": "path",
"url": "./packages/*/*/",
"options": {
"symlink": true
}
}
]
// [...]
This will let Composer include a new repository that contains our package and symlink it to the package code folder. Now to use our package in the Laravel app, add this snippet to the require section in composer.json
file:
// [...]
"require": {
"Acme/pagereview": "@dev"
}
// [...]
After that, run this command in the Laravel app directory:
$ composer update --prefer-source
This command will update all packages required and also bring in our newly developed local package.
Now, we need to add a layout file using the auth artisan command. Inside our Laravel app directory, run the command below:
$ php artisan make:auth
In the root directory of our Laravel project open the resources/views/layouts
folder and open a file called app.blade.php
. After that, add this code below the last div
tag:
<script src="{{ asset('js/app.js') }}"></script>
<script src="https://js.pusher.com/4.2/pusher.min.js"></script>
@include('pagereview::section')
- The script tag is used to fetch the Pusher.js library file.
@include
is a Laravel blade directive that allows you to include a blade view from within another view. In this case, we are including our packagesection.blade.php
view.
Now, create a new pagereview.blade.php
file inside the resources/views
folder of the Laravel app directory. After creating it, paste the snippet below:
@extends('layouts.app')
@section('content')
<div class="container">
<div class="d-flex justify-content-center">
<div>
<h1 class="mb-4">
Laravel Page Review
</h1>
<p>A simple package to add reviews to a page</p>
</div>
</div>
</div>
@endsection
Also, open routes/web.php
in the Laravel app and add this snippet:
Route::get('/test', function () {
return view('pagereview');
});
Now, open the .env
file in our Laravel app directory and update the following properties with keys from your Pusher dashboard:
PUSHER_APP_ID="PUSHER_APP_ID"
PUSHER_APP_KEY="PUSHER_APP_KEY"
PUSHER_APP_SECRET="PUSHER_APP_SECRET"
PUSHER_APP_CLUSTER="PUSHER_APP_CLUSTER"
We will use SQLite for testing our package. So, still in the same .env
file, update the .env
configurations below:
DB_CONNECTION=sqlite
DB_DATABASE=/path/to/database.sqlite
Next, create a new SQLite database by creating a file called database.sqlite
in the database
directory.
Next, run this command to migrate the database:
$ php artisan migrate
Finally, let’s serve the application using this command:
$ php artisan serve
To test that our Laravel package is working properly visit the page URL http://localhost:8000/test on two separate browser windows. Then make a review on the same page on each of the browser windows and check that it updates in realtime on the other window.
Conclusion
So far, we learned how to build a simple Laravel package using Laravel and Vue. In this tutorial, we created a reviews package for pages and also made it realtime using Pusher. which can be used in any Laravel application.
The code is available on GitHub.
24 April 2019
by Neo Ighodaro