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:
- It 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 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-
Laurent Pq said:
Really interesting article and some nice ideas. Personnaly I’m using a simpler method : In an options table I have the name of my template and in the Controller.php I’m using the View::addLocation like this :
$this->_templatePath = base_path() . ‘/resources/views/__templates/’ . $this->options[‘theme_name’][‘value’] . ‘/’;
\View::addLocation($this->_templatePath);
That’s all 🙂
If simpler and if a view is not found in the template directory it will use the default view. I have a struct like this :
views
__templates
one
blog
page
blog
page
But i really like the idea of dates then I’ll pick your idea
Thank you
Zaheer Abbas Aghani said:
Getting below error.
Call to undefined function App\Providers\resource_path()
Code :
$path = resource_path(“views/Themes/”.$theme_name.”/”.$theme_dir);
Zaheer Abbas Aghani said:
Solved it By using
“files”: [
“app/Http/Helpers/helpers.php”
],
z900collector said:
resource_path is a standard helper function, here is the URL:
https://laravel.com/docs/5.3/helpers#method-resource-path
It’s the same as using base_path(“resources/views/Themes/”.$theme_name.”/”.$theme_dir);
DevBlog said:
php I’m using the View::addLocation like this :
$this->_templatePath = base_path() . ‘/resources/views/__templates/’ .
z900collector said:
More than 1 way to skin a cat as they say. but you will find Laravel gives you a resource_path() method, so you only need to add the views/_templates bit…