Laravel: Simple Method for Modules

In my day job, I maintain a fairly large-ish Laravel application. It started out as a few separate vanilla PHP web apps, which I consolidated onto Laravel 4, then upgraded to Laravel 5. I’ve added a few new apps to the suite as business needs arose.

In the Laravel 4 version, I used creolab/laravel-modules to, well, modularize, the various apps under one Laravel instance. It worked well with a few gotchas, but the package wasn’t updated for Laravel 5. At that point I looked at a couple other module packages, but in the end decided it was simple enough to add modules to Laravel.

Assume our namespace is “CorpName”.

Add your namespace to composer.json

...
"psr-4": {
	"App\\": "app/",
	"CorpName\\": "app/CorpName/"
}
...

Create a directory structure like the following:

Add your Service Provider to the ‘providers’ array in config/app.php

...
'CorpName\Core\Providers\CorpNameServiceProvider',
...

Create “app/CorpName/Core/Providers/CorpNameServiceProvider.php”:

<?php 

namespace CorpName\Core\Providers;

use Illuminate\Support\ServiceProvider;
use CorpName\Core\Bootstrap;

/**
 * Load the CorpName Bootstrap helper, which in turn loads the modules.
 * Wired up in app/config/app.php providers array
 */
class CorpNameServiceProvider extends ServiceProvider {

	/**
	 * Bootstrap the application services.
	 *
	 * @return void
	 */
	public function boot()
	{
		$this->app['CorpName']->boot();
	}

	/**
	 * Register the application services.
	 *
	 * @return void
	 */
	public function register()
	{
		//
		$this->app->singleton('CorpName', function($app)
		{
		    return new Bootstrap($app);
		});		
	}

}

Create “app/CorpName/Core/Bootstrap.php”:

<?php

namespace CorpName\Core;

use Illuminate\Foundation\Application;

/**
 * Bootstrap the modules. Called from the ServiceProvider
 */
class Bootstrap
{
  /**
   * IoC
   * @var \Illuminate\Foundation\Application
   */
  protected $app;

  public function __construct(Application $app)
  {
    $this->app = $app;
  }

  public function boot()
  {
    $this->loadModules();
  }

  /**
   * Scan the CorpName/Modules directory for Modules, and load them (routes, views, includes)
   */
  public function loadModules()
  {
    /**
     * @var \Illuminate\Contracts\Filesystem\Filesystem $disk
     */
    $disk = \Storage::disk('modules');
    $modules = $disk->directories();

    $modules_path = config('filesystems.disks.modules.root');

    foreach ($modules as $module)
    {
      $module_path = $modules_path . '/' . $module;

      //register routes
      if($disk->exists("{$module}/routes.php"))
      {
        include $module_path . '/routes.php';
      }

      //set up views
      if($disk->exists("{$module}/views"))
      {
        $this->app['view']->addNamespace($module, $module_path . '/views');
      }

      //include files
      if($disk->exists("{$module}/includes"))
      {
        $includes = $disk->files("{$module}/includes");
        foreach ($includes as $include)
        {
          if(substr($include, -4) == '.php')
          {
            include_once $modules_path . '/' . $include;
          }
        }
      }
    }
  }
}

Note that I’m using a “modules” storage “disk” configured in “config/filesystem.php”:

...
'modules' => [
	'driver' => 'local',
	'root'   => app_path().'/CorpName/Modules',
],
...

And…

That’s basically it. You’ll probably want a routes.php file in each module, and a views directory, but you can structure your Modules as you see fit. If you use migrations, you’ll need to add your module/migrations directory to the autoload classmap in composer.json:

...
"autoload": {
	"classmap": [
		"database",
		"app/CorpName/Modules/ModuleOne/migrations",
		"app/CorpName/Modules/ModuleTwo/migrations",
		"app/CorpName/Modules/ZeeAppModule/migrations"
	],
...

Also, views can be referenced by module name in your controller and blade templates:

Controller:

...
return view('ModuleOne::index', $this->data);
...

Blade:

...
@extends('ModuleOne::layouts.master')

@section('apphead')
  @include('ModuleTwo::partials.head')
@stop
...

This setup has been running in Laravel 5.0 and 5.1 for a year or so without problems. It could probably use some improvements, like caching, but it works as is.

It's only fair to share...Share on FacebookTweet about this on TwitterEmail this to someone

Leave a Reply

Your email address will not be published. Required fields are marked *