Add-Ons & Modules

If you're looking to create a full add-on, that has it's own URL, this is the page you want. If you want to create a component that someone can include into their own views/templates, you want a widget.

At their core, the module system uses laravel-modules. The stubs are modified so the generation create the structure that is compatible with the phpVMS codebase.


To generate a module, run:

php artisan module:make {ModuleName}

Which generates the basic structure in the /modules folder. After generating the module, if you want to make it available on composer, edit the composer.json file, setting your VENDOR and author information.

All of the examples below will be based on a module named Sample

The root namespace for your module will be Modules\{YOUR_MODULE}, e.g, Modules\Sample

When a module is created, a directory structure like this is created:

├── Config
├── Console
├── Database
├── Http
├── Listeners
├── Models
├── Providers
└── Resources

This contains the config file for use in your module. The items in this will be prefixed by your module name, for example:

echo config('sample.name'); # writes out "Sample"

This contains any artisan commands your module might have

This contains several directories, the most important probably being the migrations directory. See below for more information about migrations and database access.

The folders in this are all related to HTTP access for your application - includes the controllers and routes

Any event listeners for your module will be in here. See below for more information about subscribing to events.

All of the models, used for database access, and corresponding to tables, are placed here.

Any language files and views are placed here

Still being written

To be able to publish to composer, add joshbrw/laravel-module-installer as a dependency in your module's composer.json file, and then set the type in the composer.json file to laravel-module. Then a user can run composer require your/module from Packagist to install.

See joshbrw/laravel-module-installer for additional information.


Routing follows the standard Laravel routing format, and the files are placed in the modules/{MODULE}/Http/Routes folder. For examples, see the app/Routes files to see how the Route groups work and how the middleware works. For example, the Sample module's routes:

These are in the Http/Routes/web.php file. These define your pages that render HTML. For example, the default routes look like:

Route::group(['middleware' => [
    'role:user' # Define who can access this page
]], function() {

    # all your routes are prefixed with your module's name
    # e.g. yoursite.com/sample
    Route::get('/', 'SampleController@index');
});

These are in the Http/Routes/admin.php. This will look for controllers in the Http/Controllers/Admin folder and namespace. These routes will be prefixed with $module/admin

These are in Http/Routes/api.php, and will look for controllers in the Http/Controllers/Api folder and namespace. These routes will be prefixed with /api/$module.

Middleware allows you to inject a class into the HTTP request chain, so you don't need to call a check or something in every method.

'middleware' => ['role:user']       # enable for all users
'middleware' => ['role:admin']      # enable for admin only

'middleware' => ['api.auth'] # for API routes, you can add this middlware to require API auth

Read more about Laravel Middleware

Now we can look at the (truncated) corresponding controller:

<?php

namespace Modules\Sample\Http\Controllers;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;

class SampleController extends Controller
{
    /**
     * Display a listing of the resource.
     */
    public function index()
    {
        return view('sample::index');
    }
}

$user = Auth::user();

To check if a user is logged in, use:

if(Auth::check())

Models are the more basic way to access your database tables. For example, if you have a table called sample_table, a model called SampleTable would make most sense. While table names generally refer to objects in the plural, a model is named for an item in it's singular form.

The models go into the Models directory. The fastest way is to use the artisan helper:

php artisan module:make-model SampleTable Sample

After it's generated, you should open the model, and fill in the table name. See the Laravel Model Documentation for more details on how to work with models.

If your table has a column called pirep_id, you can add a relationship to your model called pireps:

class SampleTable extends BaseModel {
    public function pirep()
    {
        return $this->belongsTo(Pirep::class, 'pirep_id');
    }
}

Now, you can easily access the parent PIREP, without having to write any queries:

$record = SampleTable::get(1);  # Get the ID of 1
echo $record->pirep->dpt_airport_id; # Write out the departure airport

We can also get fancy, since the relationship returns the Pirep model, and it has relationships to the Airport model, you can open the App\Models\Pirep file and look at the relationships.

echo $record->pirep->dpt_airport->name; # Write out the name of the departure airport

The right relationships make life easier.


Laravel includes a way to create and update tables, called migrations. Migrations are ways to programmatically define your tables, and let the framework worry about the exact syntax to use. The advantage to this abstraction is being.

There is an artisan helper to generate migrations:

php artisan module:make-migration create_sample_table ModuleName

This will create a migration file in your module's Database/migrations directory. Now, when a user can goes to the updater, at /update, it will show that there are updates available, and the migrations will be run. During an install, the migrations are also all run. This makes updates simple and straight-forward, without having to run any SQL manually.

The app/Database/migrations directory has the core migrations and is a good reference on field types, especially if you're looking to add relationships.

Design your tables well - follow the guidelines set by Laravel, and you'll have a much better time.

Add new migration files when you have to modify a table, etc, after you've released it into the wild. The migrations that are run are kept track of, so if it's seen that it's already run the file, it won't run it again.

I've added a few extra features, including adding seed data, including adding seeder data. For example, the Settings table:

class CreateSettingsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('settings', function (Blueprint $table) {
            // ... Create all the columns
        });

            // Create some initial data, with the columns filled out
        $settings = [
            [
                'order' => 1,
                'name' => 'Start Date',
                'group' => 'general',
                'value' => '',
                'type' => 'date',
                'description' => 'The date your VA started',
            ],

            // A lot more entries

        ];

        $this->addData('settings', $settings);
    }

    // Not showning the down()
}

Templates are placed in modules/{ModuleName}/Resources/views. If someone wants to modify the views, it's recommended that they either run php artisan vendor:publish or they copy the templates into the resources/views/module/$moduleName folder (look at the $viewPath value in the registerViews() method in the $MODULE/Providers/$ModuleServiceProvider.php file for the exact path if you're unsure).

Read more about Laravel Blade Templating


Available events from the main app are listed in the app/Events directory. Subscribing to events follows the Laravel Events format. Create the event listener in your $MODULE\Listeners folder (e.g, PirepAcceptedListener), and then register it in your $MODULE\Providers\EventServiceProvider folder, in the $listen section, like:

protected $listen = [
    'App\Events\PirepAccepted' => [
        'Modules\Sample\Listeners\PirepAcceptedListener',
    ],
];

To see the data passed, just look in the Event class.


When multiple models/repositories/steps are involved in a task, they should be written as a Service class. For example, when filing a PIREP, there are changes made to the PIREP model, but also to the User model, one or more events are dispatched, etc. Instead of putting all of this logic into a Controller or directly into a Model, a Service class should be defined, which ties all these steps together. This helps with testing and debugging, and portability. In the PIREP example, it can be filed through a web interface, or a RESTful interface. A Service class allows for both of these to use the same logic without reusing code.

While you can use and import models directly, it's recommended to use the repositories, in the app/Repositories are listed here. Repositories give the added benefits of automatically caching and flushing the cache when data is added/updated.

The recommended method is to use Automatic Injection, which would involve adding a constructor to your Listener, as such:

namespace Modules\Sample\Listeners;

use App\Events\PirepAccepted;
use App\Repositories\PirepRepository;

class PirepAcceptedListener {

    private $pirepRepo;

    // You can pass as many Repositories in as your wish. 
    // See the app\Controllers for more examples
    public function __construct(PirepRepository $pirepRepo) {
        $this->pirepRepo = $pirepRepo;
    }

    public function handle(PirepAccepted $pirep) {
        Log::info('Received PIREP', [$pirep]);
    }
}

The methods in the repositories largely mirror the Model methods, but can automatically handle searches, etc. The docs for the repositories are available here. You can read more about the repository pattern here