Introduction

Over the last year I’ve been developing a new shopping cart using Laravel. The design goals not only include being fast with a GT Metrics rating of AA and 90%+ but also to have all the bells and whistles of a large scale Magento Enterprise site but geared for the small to medium sized business. The app will feature a full Business Automation System, Customer Management and a host of other features you would find on an Enterprise installation.

In particular the easy to use Laravel Blade templates means it will be easy to develop themes and perhaps an eco-system will evolve from it. I’m building it so that I can phase out a bunch of old Magento sites and move then to a much simpler design using a modern technology stack such as Laravel, in this tutorial I am using Laravel v5.3

The Theme design is simple, in the resources/views directory there is a directory called “Themes” and under that a “default” directory. A “default” theme is the minimal requirement and will ship with the release Candidate due shortly. To add a theme, you simply create a sub-directory under “Themes” and provide the required directories for the various components the software requires. If you only provide a sub-set of the directories then the “default” theme components are used instead.

The default theme directories (located under resources/views/Themes/default) are:

  • Home
  • Header
  • Footer
  • Errors
  • Includes
  • Support
  • Product
  • Cart
  • Category

Additional directories can be added over time and you have the flexibility to craft your own directory structures.

Themes are activated based on the date, so you can stage well in advance the look and feel of the site for various festival times of the year like Christmas, Halloween, New Years Eve etc. This is controlled from a database table called “themes”. When no theme applies the “default” is automatically used.

Database Migration

Starting with the database table, there are a minimal of 3 fields required, a theme name (case sensitive), and a starting date and ending date the theme is active for. When no theme is found, the default is active.

Using “artisan” we can create our Model and Migration template as such:

php artisan make:model Theme –migration

After defining the fields, the migration file looks like this:

<?php

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateThemesTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
    Schema::create(‘themes’, function (Blueprint $table) {
      $table->increments(‘id’);
      $table->string(‘theme_name’);
      $table->date(‘theme_date_from’);
      $table->date(‘theme_date_to’);
      $table->integer(‘theme_store_id’)->unsigned()->default(0);
      $table->timestamps();
});
}

/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
   Schema::dropIfExists(‘themes’);
}
}

I have used the migration from the code base so don’t worry about the “theme_store_id” column, we are only interested in the name and date columns. Once you have created the migration file you can run it using:

php artisan migrate

The database table should create with no issues and look like this:

MariaDB [teststore]> desc themes;
+-----------------+------------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-----------------+------------------+------+-----+---------+----------------+
| id | int(10) unsigned | NO | PRI | NULL | auto_increment |
| theme_name | varchar(255) | NO | | NULL | |
| theme_date_from | date | NO | | NULL | |
| theme_date_to | date | NO | | NULL | |
| theme_store_id | int(10) unsigned | NO | | 0 | |
| created_at | timestamp | YES | | NULL | |
| updated_at | timestamp | YES | | NULL | |
+-----------------+------------------+------+-----+---------+----------------+
7 rows in set (0.01 sec)

Model Code

The Model code created by artisan is pretty bare, I added one line of code, the $table variable. And changed the namespace to App\Models as I keep all my Model code in app/Models.

<?php
/**
* @author Sid Young
* @date 2017-08-29
*
*/
namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Theme extends Model
{
protected $table = “themes”;
}

Service Provider

The real guts of getting the correct theme to apply is in the service provider. I researched for a solution on both forums and Google to little avail so I crafted the following as a logical way to get and apply a theme. Appropriately enough its called ThemeServiceProvider.php and lives in the app/Providers directory. The provider has three tasks it must do:

  1. It has to read our theme table using a single Eloquent call, to get all themes then,
  2. Using today’s date, work out the theme name to apply and finally,
  3. Determine the correct Theme paths and create the global variable for BOTH the Views and Controllers that need to know about where the blade templates are located.

The working Provider code is as follows:

<?php
/**
* @author Sid Young <sid AT off-grid-engineering DOT com >
* @date 2017-08-28
* \class ThemeServiceProvider
*
*
* [CC]
*/
namespace App\Providers;

use Illuminate\Support\Facades\View;
use Illuminate\Support\ServiceProvider;
use App\Models\Theme;

/**
* \brief Provides a series of global variables that can be used to access the required Theme Directory structure
*/
class ThemeServiceProvider extends ServiceProvider
{

/**
* Assign our theme paths, point to default theme path if named theme does not have the required path.
* That way you can implement but some components as needed (like a different header at certain times of the year).
*
* @return void
*/
public function boot()
{
$theme_name = “default”;
$today = strtotime(date(“Y-m-d”));
#
# 1. Get theme names from DB table
#
$theme_data = Theme::all();
foreach($theme_data as $t)
{
  #
  # 2. Test for the required date range.
  #
  if(($today >= strtotime($t->theme_date_from)) && ($today <=strtotime($t->theme_date_to)))
  {
    $theme_name = $t->theme_name;
    break;
  }
}
View::share(“THEME_NAME”, $theme_name);
#
# 3. Build Paths, test and set path, if dir does not exist then set the default path.
#
$theme_directories = array(“Home”,”Header”,”Footer”,”Support”,”Includes”,”Errors”,”Product”,”Category”,Cart);
foreach($theme_directories as $theme_dir)
{
   $theme_u_dir = “THEME_”.strtoupper($theme_dir);
   $blade_path = “Themes.”.$theme_name.”.”.$theme_dir.”.”;
   $default_blade_path = “Themes.default.”.$theme_dir.”.”;

    $path = resource_path(“views/Themes/”.$theme_name.”/”.$theme_dir);
    #echo “PATH: “.$path.”<br>”;
    if(file_exists($path) && is_dir($path))
    {
      View::share($theme_u_dir, $blade_path);
      \Config::set($theme_u_dir , $blade_path);
    }
    else
    {
       View::share($theme_u_dir, $default_blade_path);
       \Config::set($theme_u_dir, $default_blade_path);
    }
  }
}

  /**
   * Register any application services.
   *
   * @return void
   */
   public function register()
   {
   }
}

To make the Theme variables available to the application we use the View:share() method, this enables every directory we need to be defined, if the theme directory does not exist, we assign it the default. So the base theme path becomes:

  • Themes/default
  • Themes/<your_theme_name>
  • Themes/<next_theme_name>

To make the theme variables available in our Controller we use the \Config::set() and \Config::get() methods. So the Theme Home variable becomes THEME_HOME and points to Themes/<your_theme_name>/Home but returns a string formatted for accessing a Blade, “Themes.<your_theme_name>.Home.”, note the trailing “.”, its for convenience when constructing Blade paths.

So if we created a theme called “halloween”, THEME_HOME would be:

Themes.halloween.Home.

We only need to add onto the end the view located in the /Themes/halloween/Home/ directory to get the Blade to render.

The last step for the Service Provider is to enable it in our app.php config file, using “vi” or your favourite editor, open the config/app.php file and add this line to the end of the “providers” array:

App\Providers\ThemeServiceProvider::class,

Save the file and test your development site, it should continue to render without error.

Controller Code

The Controller code needs to know the theme variable in order to call the appropriate view. Where we would normally invoke a view with something like:

return view(‘Product.productpage’,[‘product’=>$product]);

We now use a Config:get(‘THEME_PRODUCT’) call like so:

$route = \Config:get(‘THEME_PRODUCT’).’productpage’;
return view($route,[‘product’=>$product]);

And that’s it in the Controller! Every time we need to call a view we just use the theme config variable that automatically set for us. No hard coding of theme paths and flexibility to implement a full theme or any subset by letting the Service Provider do the hard work for us.

View Implementation

The real simplicity of this approach is in the view, using Laravels Blade templates we can easily return the THEME variables inside any blade and use them in Blade directives.

To test the correct processing of the theme variables, I created a simple blade in the resources/views directory, it looks like this:

views]# cat themeinfo.blade.php

<table cellpadding=5 cellspacing=5>
<tr><td>Theme Name</td><td>{{ $THEME_NAME }}</td></tr>
<tr><td>Home</td><td>{{ $THEME_HOME }}</td></tr>
<tr><td>Header</td><td>{{ $THEME_HEADER }}</td></tr>
<tr><td>Footer</td><td>{{ $THEME_FOOTER }}</td></tr>
</table>
views]#

To invoke the test blade, I added a single GET route to my routes file:

Route::get(‘/themeinfo’, function() { return View::make(‘themeinfo’); });

As you can see the Blade uses the same {{ }} variables for any passed variables thanks to the use of the View::share() method in the Provider code. The output from the URL call gives us:

Theme Name test
Home Themes.default.Home.
Header Themes.default.Header.
Footer Themes.default.Footer.

Now lets add a theme, for this tutorial I will just do an insert in mysql:

MariaDB [teststore]> insert into themes values (0,’test’,’2017-08-29′,’2017-10-30′,0,0,0);
Query OK, 1 row affected (0.01 sec)

Now I will create the theme directory but only the Footer directory for testing and create a footer file with a couple of test lines:

resources/views#mkdir -p Themes/test/Footer
resources/views#vi Themes/test/Footer/footer.blade.php
<br/>
<br/>
I’m the Test footer used in {{ THEME_NAME }}
resources/views#

Re-running our themeinfo blade test we now get:

Theme Name test
Home Themes.default.Home.
Header Themes.default.Header.
Footer Themes.test.Footer.

Note: To get the same results, adjust the dates in the insert SQL so today falls in the required date range.

My application calls the Theme/default/Home/storefront.blade.php file when the “/” page is called. Using our new “test” theme, lets add a new test homepage in Themes/test/Home/storefront.blade.php

resources/views#vi Themes/test/Home/storefront.blade.php
<p>I’m the test Homepage</p>
.
.
resources/views#

Calling the web site with no URL parameters now returns our message, so lets include the new Footer and we how we use the THEME_FOOTER variable in a Blade directive.

resources/views#vi Themes/test/Home/storefront.blade.php
<p>I’m the test Homepage</p>
.
@include( $THEME_FOOTER.’footer’)
resources/views#

Calling our home page gives us:

Test StoreFront

I’m the test footer

That’s it! You can use the $THEME_XXXXX variables in any directive and the string assigned will be returned!

The List of THEME_XXX variables created is defined in the Service Provider, the current list is:

  • THEME_NAME
  • THEME_HOME
  • THEME_FOOTER
  • THEME_HEADER
  • THEME_PRODUCT
  • THEME_CATEGORY
  • THEME_CART
  • THEME_ERRORS
  • THEME_CART

You can extend this as you need by adding more directory names in the Service Provider code.

Enjoy!

-oOo-