← Back to the Blog

Laravel 5 Implementing Scheduled Themes

By Sid Young
Laravel 5 Implementing Scheduled Themes

Introduction

Here at Conetix we have been using the Laravel PHP Framework for many internal business automation projects for the last 2 years, we have also noticed a growing range of software coming on the market using the Laravel Framework. We host a lot of WordPress sites (also Joomla, Drupal, Magento) and as these are all PHP based like Laravel, it seams natural for us to be able to support it and use it. As our skill set has risen so has the need to implement features not found natively in the framework.

Recently I wrote a number of articles on Laravel, some have been geared towards refactoring such as this article "Developing applications with Laravel 5 - Part 2". In fact I find each release of Laravel requires me to do some degree of refactoring and as I learn more and more about the framework I go back and refactor a lot of code removing big chunks of code that is already provided in the framework. In this article, I'm going to outline a simple date based mechanism for rendering themes in any Laravel Application. For this tutorial I have used Laravel v5.3 and the code does not require any additional packages other than what comes standard.

To implement Themes, the design concept is simple, use a directory in the resources/views directory called "Themes" and under that a "default" directory to hold our basic fully functional application code prior to any theme specific code. To add another 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.

For this tutorial, the default theme directories (located under resources/views/Themes/default) are:

  • Home
  • Header
  • Footer
  • Errors
  • Includes
  • Support

Additional directories can be added by you over time as your application evolves, so you have the flexibility to craft your own directory structures.

In this tutorial, Themes are activated based on the date, but there is no reason why you cannot use a field in your "users" table to add a theme name and specify a "per user theme". I will outline it in the provider code below. But I've used the date based mechanism so you can use the code in an eCommerce application, that way 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

For this tutorial we first need to have a database table to hold the theme name (case sensitive), a starting date and ending date the theme is active for. Keep in mind, when a theme is not found, the default is active.

Using the "artisan" tool, 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->timestamps();
        });
    }

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

After you have created the migration file, you can run it using the following command:

#php artisan migrate

The database table should be created 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 | |
| created_at | timestamp | YES | | NULL | |
| updated_at | timestamp | YES | | NULL | |
+-----------------+------------------+------+-----+---------+----------------+
7 rows in set (0.01 sec)

Model Code

The Model code created by the Laravel "Artisan" tool is pretty basic, 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. See my previous article on Namespaces In Laravel Applications

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

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

Service Provider

As hinted to earlier, the correct theme to apply is determined in the service provider.  Laravel service providers live in the app/Providers directory, and for our tutorial I have called ours "ThemeServiceProvider.php".

The provider has to read our theme table using a single Eloquent call, to get all themes then, using today's date, work out the theme name to apply and finally determine the correct Theme paths and create the global variables for the Views and Controllers that need to know about where the blade templates are located.

If you were to build a per user based theme, then you could get the user ID, read the users table and set the $theme_name variable to the desired theme and not process any date specific code, the rest of the path determining code remains the same.

The working Provider code is as follows:

<?php
/**
* @author Sid Young
* @date 2017-09-04
* \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.
* See notes above for user based themes rather than date based themes.
*/
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, 
#    For "user" based themes, read the user table for the logged in user.
#
$theme_data = Theme::all();
foreach($theme_data as $t)
{
  #
  # 2. Test for the required date range.
  #    For "user" based themes, comment this out and just set the $theme_name variable.
  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");
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_XXXXX" 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 to the equivalent "default" structure.

An example of the theme paths are:

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

To make the theme variables available in our Controller we use the \Config::set() in the Provider and \Config::get() method in the Controller(s). 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 "modern", THEME_HOME would be:

            Themes.modern.Home.

We only need to add onto the end the view located in the /Themes/modern/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('Users.testpage',['mydata'=>$mydata]);

We now use a Config:get('THEME_HOME') call like so:

$route = \Config:get('THEME_HOME').'Users.testpage';
return view($route,['mydata'=>$mydata]);

Every time we need to call a view inside a Controller, we just use the theme configuration variables that are automatically set for us. No hard coding of theme paths and we still have the 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 becomes apparent in the view files. Using Laravel's 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/>
<b>I'm the Test footer</b> 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/frontend.blade.php file when the "/" page is called. Using our new "test" theme, lets add a new test homepage in Themes/test/Home/frontend.blade.php

resources/views#vi Themes/test/Home/frontend.blade.php
<p>I'm the test Frontend</p>
.
.
resources/views#

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

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

Calling our home page gives us:

I'm the test Frontend

I'm the test footer used in test

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_ERRORS

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

Enjoy!

-oOo-