Laravel 4 Multisites

A Comprehensive Tutorial


Introduction

Laravel 4 is a huge step forward for the PHP community. It’s beautifully written, full of features and the community is presently exploding. Today we’re going to explore how to build multisite applications with it.

I have spent two full hours getting the code out of a Markdown document and into Medium. Medium really isn’t designed for tutorials such as this, and while much effort has been spent in the pursuit of accuracy; there’s a good chance you could stumble across a curly quote in a code listing. Please make a note and I will fix where needed.
I have also uploaded this code to Github. You need simply follow the configuration instructions in this tutorial, after downloading the source code, and the application should run fine. This assumes, of course, that you know how to do that sort of thing. If not; this shouldn’t be the first place you learn about making PHP applications.
https://github.com/formativ/tutorial-laravel-4-multisites
If you spot differences between this tutorial and that source code, please raise it here or as a GitHub issue. Your help is greatly appreciated.

Installing Laravel 4

Laravel 4 uses Composer to manage its dependencies. You can install Composer by following the instructions at http://getcomposer.org/
doc/00-intro.md#installation-nix
.

Once you have Composer working, make a new directory or navigation to an existing directory and install Laravel 4 with the following command:

composer create-project laravel/laravel ./ --prefer-dist

If you chose not to install Composer globally (though you really should), then the command you use should resemble the following:

php composer.phar create-project laravel/laravel ./ --prefer-dist

Both of these commands will start the process of installing Laravel 4. There are many dependencies to be sourced and downloaded; so this process may take some time to finish.

Note on Operating Systems

I work on a Macbook, and deal with Linux servers every day. I seldom interact with Windows-based machines. As a result; most of the work I do is never run on Windows. Remember this as you follow this tutorial — I will not show how to do things on Windows because it’s dead to me. If you are forced to use it; then you have my sympathies.

Note on Server Setup

We’re going to look at how to create virtual hosts in Apache2 and Nginx, but we won’t look at how to get those systems running in the first place. It’s not something I consider particularly tricky, and the internet is filled with wonderful tutorials on the subject.

Note on Dutch

I use it in this tutorial, but I don’t really speak it. Google Translate does. Blame Google Translate.

Virtual Hosts

It may surprise you to know that single domain names do not translate into single web servers. Modern web servers have the ability to load many different domains, and these domains are often referred to as Virtual Hosts or Virtual Domains. We’re going to see how to use them with Laravel 4.

Adding Virtual Host Entries

When you type an address into your browser, and hit enter; your browser goes through a set of steps in order to get you the web page you want. First, it queries what’s called a hosts file to see if the address you typed in is a reference to a local IP address. If not found, the browser then queries whatever DNS servers are available to the operating system.

A DNS server compares an address (e.g. example.com) with a list of IP addresses it has on file. If an IP address is found; the browser’s request is forwarded to it and the web page is delivered.

This is a rather simplified explanation of the steps involved. You can probably find more details at: http://en.wikipedia.org/wiki/Internet.

In a sense; the hosts file acts as a DNS server before any remote requests are made. It follows that the best place to add references to local IP addresses (local web servers) is in the hosts file.

To do that, open the hosts file:

sudo vim /etc/hosts
If that command gives you errors then you can try running it without the sudo part. Sudo is (simply speaking) a way to run a command at the highest permission level possible; to ensure it executes correctly. If may be that you do not have sufficient privileges to run commands as root (with sudo).
Vim is a teminal-based text editor. If you’re uneasy using it; what we’re after is editing /etc/hosts.

On a new line, at the bottom of /etc/hosts, add:

127.0.0.1 dev.tutorial
This was extracted from /etc/hosts for brevity.

This line tells the operating system to send requests to dev.tutorial to 127.0.0.1. You’ll want to replace 127.0.0.1 with the IP address of the server you are using for this tutorial (assuming it’s not LAMP/MAMP on your local machine) and dev.tutorial with whatever you want the virtual host to be.

You’ll need to repeat this process for each virtual host you add. If your application has (for example) three different domains pointing to it; then you will need to set three of these up to test them locally.

Creating Apache 2 Virtual Hosts

Apache2 virtual host entries are created by manipulating the configuration files usually located in /etc/apache2/sites-available/. You can edit the default file, and add the entries to it, or you can create new files.

If you choose to create new files for your virtual host entries, then you will need to symlink them to /etc/apache2/sites-enabled/* where * matches the name of the files you create in the sites-available folder.

The expanded syntax of virtual host entries can be quite a bit to type/maintain; so I have a few go-to lines I usually use:

<VirtualHost *:80>
DocumentRoot /var/www/site1
ServerName dev.site1
</VirtualHost>
This was extracted from /etc/apache2/sites-available/default for brevity.

The ServerName property can be anything you like — it’s the address you will use in your browser. The DocumentRoot property needs to point to an existing folder in the filesystem of the web server machine.

You can learn more about virtual host entries, in Apache2, at: http://httpd.apache.org/docs/current/vhosts/examples.html.

Creating Nginx Virtual Hosts

Similar to Apache2, Nginx virtual host entries are created by manipulating the configuration files usually located in /etc/nginx/sites-available/. You can edit the default file, and add the entries to it, or you can create new files.

If you choose to create new files for your virtual host entries, then you will need to symlink them to /etc/nginx/sites-enabled/* where * matches the name of the files you create in the sites-available folder.

The expanded syntax of virtual host entries can be quite a bit to type/maintain; so I have a few go-to lines I usually use:

server {
listen 80;
server_name dev.site1;
root /var/www/site1;
index index.php;

location ~ \.php$ {
try_files $uri =404;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass unix:/var/run/php5-fpm.sock;
fastcgi_index index.php
include fastcgi_params;
}
}
This was extracted from /etc/nginx/sites-available/default for brevity.
This site definition assumes you are using PHP5 FPM. Getting this installed and running is out of the scope of this tutorial, but there are plenty of great tutorials on the subject.
You can learn more about virtual host entries, in Nginx, at: https://www.digitalocean.com/community/articles/how-to-set-up-nginx-virtual-hosts-server-blocks-on-ubuntu-12-04-lts--3.

Environments

Laravel 4 employs a system of execution environments. Think of these as different contexts in which different configuration files will be loaded; determined by the host name of the machine or command flags.

To illustrate this concept; think of the differences between developing and testing your applications locally and running them on a production server. You will need to target different databases, use different caching schemes etc.

This can be achieved, deterministically, by setting the environments to match the host names of the machines the code will be executed (local and production respectively) and having different sets of configuration files for each environment.

When a Laravel 4 application is executed, it will determine the environment that you are running it in, and adjust the path to configuration files accordingly. This approach has two caveats.

The first caveat is that the PHP configuration files in app/config are always loaded and environment-based configuration files are then loaded on top of them. If you have a database set up in app/config/
database.php
and nothing in app/config/production/database.php, the global database configuration details will apply.

The second caveat is that you can override the environment Laravel 4 would otherwise use by supplying an environment flag to artisan commands:

php artisan migrate --env=local

This will tell artisan to execute the database migrations in the local environment, whether or not the environment you are running it in is local.

This is important to multisites because Laravel 4 configuration files have the ability to connect to different database, load different views and send different emails based on environmental configuration.

Note on Running Commands in Local Environment

The moment you add multiple environments to your application, you create the possibility that artisan commands might be run on the incorrect environment.

Just because Laravel 4 is capable of executing in the correct environment doesn’t mean you will always remember which environment you are in or which environment you should be in…

Start learning to provide the environment for every artisan command, when you’re working with multiple environments. It’s a good habit to get into.

Using Site-Specific Views

One of the benefits of multiple environment-specific configuration sets is that we can load different views for different sites. Let’s begin by creating a few:

127.0.0.1 dev.www.tutorial-laravel-4-multisites
127.0.0.1 dev.admin.tutorial-laravel-4-multisites
This was extracted from /etc/hosts for brevity.

You can really create any domains you want, but for this part we’re going to need multiple domains pointing to our testing server so that we can actually see different view files being loaded, based on domain.

Next, update your app/bootstrap/start.php file to include the virtual host domains you’ve created:

$env = $app->detectEnvironment([
"www" => ["dev.www.tutorial-laravel-4-multisites"],
"admin" => ["dev.admin.tutorial-laravel-4-multisites"]
]);
This was extracted from /bootstrap/start.php for brevity.
Phil Sturgeon made an excellent point about not using URL’s to toggle environments, as somebody can easily set a hostname to match your development environment and point it to your production environment to cause unexpected results. This can lead to people gleaning more information than you would expect via debugging information or backtraces.

If you would rather determine the current environment via a server configuration property; you can pass a callback to the detectEnvironment() method:

$env = $app->detectEnvironment(function()
{
return Input::server("environment", "development");
});
This was extracted from /bootstrap/start.php for brevity.

Clean out the app/views folder and create www and admin folders, each with their own layout.blade.php and index/index.blade.php files.

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Laravel 4 Multisites</title>
</head>
<body>
@yield("content")
</body>
</html>
This file should be saved as app/views/www/layout.blade.php.
@extends("layout")
@section("content")
Welcome to our website!
@stop
This file should be saved as app/views/www/index/index.blade.php.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Laravel 4 Multisites — Admin</title>
</head>
<body>
@yield("content")
</body>
</html>
This file should be saved as app/views/admin/layout.blade.php.
@extends("layout")
@section("content")
Please log in to use the admin.
@stop
This file should be saved as app/views/admin/index/index.blade.php.

Let’s also prepare the routes and controllers for the rest of the tutorial, by updating both:

<?php
Route::any("/", [
"as" => "index/index",
"uses" => "IndexController@indexAction"
]);
This file should be saved as app/routes.php.
<?php
class IndexController
extends BaseController
{
public function indexAction()
{
return View::make("index/index");
}
}
This file should be saved as app/controllers/IndexController.php.

In order for Laravel to know which views to use for each environment, we should also create the configuration files for the environments.

<?php
return [
"paths" => [app_path() . "/views/www"]
];
This file should be saved as app/config/www/view.php.
<?php
return [
"paths" => [app_path() . "/views/admin"]
];
This file should be saved as app/config/admin/view.php.

These new configuration files tell Laravel 4 not only to look for views in the default directory but also to look within the environment-specific view folders we’ve set up. Going to each of these virtual host domains should now render different content.

I would recommend this approach for sites that need to drastically change their appearance in ways mere CSS couldn’t achieve. If, for example, your different sites need to load different HTML or extra components then this is a good approach to take.

Another good time to use this kind of thing is when you want to separate the markup of a CMS from that of a client-facing website. Both would need access to the same business logic and storage system, but their interfaces should be vastly different.

I’ve also used this approach when I’ve needed to create basic mobile interfaces and rich interactive interfaces for the same brands…

You can learn more about environments at: http://laravel.com/docs/
configuration#environment-configuration
.

Using Site-Specific Routes

Another aspect to multiple-domain applications is how they can affect (and be affected by) the routes file. Consider the following route groups:

Route::group([
"domain" => "dev.www.tutorial-laravel-4-multisites"
], function()
{
Route::any("/about", function()
{
return "This is the client-facing website.";
});
});
Route::group([
"domain" => "dev.admin.tutorial-laravel-4-multisites"
], function()
{
Route::any("/about", function()
{
return "This is the admin site.";
});
});
This was extracted from app/routes.php for brevity.

Aside from the basic routes Laravel 4 supports, it’s also possible to group routes within route groups. These groups provide an easy way of applying common logic, filtering and targeting specific domains.

Not only can we explicitly target our different virtual host domains, we can target all subdomains with sub-domain wildcard:

Route::group([
"domain" => "dev.{sub}.tutorial-laravel-4-multisites"
], function()
{
Route::any("/whoami", function($sub)
{
return "You are in the '" . $sub . "' sub-domain.";
});
});
This was extracted from app/routes.php for brevity.

This functionality allows some pretty powerful domain-related configuration and logical branching!

Translation

Laravel 4 includes a translation system that can greatly simplify developing multisites. Translated phrases are stored in configuration files, and these can be returned in views.

Using Language Lookups

The simplest example of this requires two steps: we need to add the translated phrases and we need to recall them in a view. To begin with; we’re going to add a few English and Dutch phrases to the configuration files:

<?php
return [
"instructions" => "Follow these steps to operate cheese:",
"step1" => "Cut the cheese.",
"step2" => "Eat the :product!",
"product" => "cheese"
];
This file should be saved as app/lang/en/steps.php.
<?php
return [
"instructions" => "Volg deze stappen om kaas te bedienen:",
"step1" => "Snijd de kaas.",
"step2" => "Eet de :product!",
"product" => "kaas"
];
This file should be saved as app/lang/nl/steps.php.
<?php
class IndexController
extends BaseController
{
public function indexAction()
{
App::setLocale("en");
        if (Input::get("lang") === "nl")
{
App::setLocale("nl");
}
        return View::make("index/index");
}
}
This file should be saved as app/controllers/IndexController.php.

This will toggle the language based on a querystring parameter. The next step is actually using the phrases in views:

@extends("layout")
@section("content")
<h1>
{{ Lang::get("steps.instructions") }}
</h1>
<ol>
<li>
{{ trans("steps.step1") }}
</li>
<li>
{{ trans("steps.step2", [
"product" => trans("steps.product")
]) }}
</li>
</ol>
@stop
This file should be saved as app/views/www/layout.blade.php.

The Lang::get() method gets translated phrases out of the configuration files (in this case steps.instructions). The trans() method serves as a helpful alias to this.

You may also have noticed that step2 has a strange placeholder (:product). This allows the insertion of variable data into translation phrases. We can pass these in the optional second parameter of Lang::get()/trans().

You can learn more about the Localization class at: http://laravel.com/
docs/localization
.

Using Language Lookups in Packages

We’re not going to go into the details of How To Create Packages in Laravel 4, except to say that it’s possible to have package-specific translation. If you’ve set a package up, and registered its service provider in the application configuration, then you should be able to insert the following lines:

public function boot()
{
$this->package("formativ/multisite", "multisite");
}
This was extracted from workbench/formativ/multisite/src/
Formativ/Multisite/MultisiteServiceProvider.php
for brevity.

…in the service provider. Your package will probably have a different vendor/package name, and you need to pay particular attention to the second parameter (which is the alias to your package assets).

Add these translation configuration files also:

<?php
return [
"instructions" => "Do these:"
];
This file should be saved as workbench/formativ/multisite/src/
lang/en/steps.php
.
<?php
return [
"instructions" => "Hebben deze:"
];
This file should be saved as workbench/formativ/multisite/src/
lang/nl/steps.php
.

Finally, let’s adjust the view to reflect the new translation phrase’s location:

<h1>
{{ Lang::get("multisite::steps.instructions") }}
</h1>
This was extracted from app/views/www/index/index.blade.php for brevity.

It’s as easy at that!

You can learn more about package resources at: http://laravel.com/
docs/packages#package-configuration
.

Caching Language Lookups

Getting translated phrases from the filesystem can be an expensive operation, in a big system. What would be even cooler is if we could use Laravel 4's built-in cache system to make repeated lookups more efficient.

To do this, we need to create a few files, and change some old ones:

// 'Lang' => 'Illuminate\Support\Facades\Lang',
"Lang" => "Formativ\Multisite\Facades\Lang",
This was extracted from app/config/app.php for brevity.

This tells Laravel 4 to load our own Lang facade in place of the one that ships with Laravel 4. We’ve got to make this facade…

<?php
namespace Formativ\Multisite\Facades;
use Illuminate\Support\Facades\Facade;
class Lang
extends Facade
{
protected static function getFacadeAccessor()
{
return "multisite.translator";
}
}
This file should be saved as workbench/formativ/multisite/src/
Formativ/Multisite/Facades/Lang.php
.

This is basically the same facade that ships with Laravel 4, but instead of returning translator it will return multisite.translator. We need to register this in our service provider as well:

public function register()
{
$this->app["multisite.translator"] =
$this->app->share(function($app)
{
$loader = $app["translation.loader"];
$locale = $app["config"]["app.locale"];
$trans = new Translator($loader, $locale);
        return $trans;
});
}
This was extracted from workbench/formativ/multisite/src/
Formativ/Multisite/MultisiteServiceProvider.php
for brevity.

I’ve used very similar code to what can be found in vendor/laravel/
framework/src/Illuminate/Translation/TranslationServiceProvider.php
. That’s because I’m still using the old file-based loading system to get the translated phrases initially. We’ll only return cached data in subsequent retrievals.

Lastly; we need to override how the translator fetches the data.

<?php
namespace Formativ\Multisite;
use Cache;
use Illuminate\Translation\Translator as Original;
class Translator
extends Original
{
public function get($key, array $replace = array(),
$locale = null)
{
$cached = Cache::remember($key, 15,
function() use ($key, $replace, $locale)
{
return parent::get($key, $replace, $local
});
        return $cached;
}
}
This file should be saved as workbench/formativ/multisite/src/
Formativ/Multisite/Translator.php
.

The process is as simple as subclassing the Translator class and caching the results of the first call to the get() method.

You can learn more about facades at: http://laravel.com/docs/facades.

Creating Multi-Language Routes

Making multi-language routes may seem needless, but link are important to search engines, and the users who have to remember them.

To make multi-language routes, we first need to create some link terms:

<?php
return [
"cheese" => "cheese",
"create" => "create",
"update" => "update",
"delete" => "delete"
];
This file should be saved as app/lang/en/routes.php.
<?php
return [
"cheese" => "kaas",
"create" => "creëren",
"update" => "bijwerken",
"delete" => "verwijderen"
];
This file should be saved as app/lang/nl/routes.php.

Next, we need to modify the app/routes.php file to dynamically create the routes:

$locales = [
"en",
"nl"
];
foreach ($locales as $locale)
{
App::setLocale($locale);
    $cheese = trans("routes.cheese");
$create = trans("routes.create");
$update = trans("routes.update");
$delete = trans("routes.delete");
    Route::any($cheese . "/" . $create,
function() use ($cheese, $create)
{
return $cheese . "/" . $create;
});
    Route::any($cheese . "/" . $update,
function() use ($cheese, $update)
{
return $cheese . "/" . $update;
});
    Route::any($cheese . "/" . $delete,
function() use ($cheese, $delete)
{
return $cheese . "/" . $delete;
});
}
This was extracted from app/routes.php for brevity.

We hard-code the locale names because it’s the most efficient way to return them in the routes file. Routes are determined on each application request, so we dare not do a file lookup…

What this is basically doing is looping through the locales and creating routes based on translated phrases specific to the locale that’s been set. It’s a simple, but effective, mechanism for implementing multi-language routes.

If you would like to review the registered routes (for each language), you can run the following command:

php artisan routes
You can learn more about routes at: http://laravel.com/docs/routing.

Creating Multi-Language Content

Creating multi-language content is nothing more than having a few extra database table fields to hold the language-specific data. To do this; let’s make a migration and seeder to populate our database:

<?php
use Illuminate\Database\Migrations\Migration;
class CreatePostTable
extends Migration
{
public function up()
{
Schema::create("post", function($table)
{
$table->increments("id");
$table->string("title_en");
$table->string("title_nl");
$table->text("content_en");
$table->text("content_nl");
$table->timestamps();
});
}
    public function down()
{
Schema::dropIfExists("post");
}
}
This file should be saved as app/database/migrations/
0000_00_00_000000_CreatePostTable.php
.
<?php
class DatabaseSeeder
extends Seeder
{
public function run()
{
Eloquent::unguard();
$this->call("PostTableSeeder");
}
}
This file should be saved as app/database/seeds/DatabaseSeeder.php.
<?php
class PostTableSeeder
extends DatabaseSeeder
{
public function run()
{
$posts = [
[
"title_en" => "Cheese is the best",
"title_nl" => "Kaas is de beste",
"content_en" => "Research has shown...",
"content_nl" => "Onderzoek heeft aangetoond..."
]
];
        DB::table("post")->insert($posts);
}
}
This file should be saved as app/database/seeds/PostTableSeeder.php.

To get all of this in the database, we need to check the settings in app/config/database.php and run the following command:

php artisan migrate --seed --env=local

This should create the post table and insert a single row into it. To access this table/row, we’ll make a model:

<?php
class Post
extends Eloquent
{
protected $table = "post";
}
This file should be saved as app/models/Post.php.

We’ll not make a full set of views, but let’s look at what this data looks like straight out of the database. Update your IndexController to fetch the first post:

<?php
class IndexController
extends BaseController
{
public function indexAction($sub)
{
App::setLocale($sub);
        return View::make("index/index", [
"post" => Post::first()
]);
}
}
This file should be saved as app/controllers/IndexController.php.

Next, update the index/index.blade.php template:

<h1>
{{ $post->title_en }}
</h1>
<p>
{{ $post->content_en }}
</p>
This was extracted from app/views/www/index/index.blade.php for brevity.

If you managed to successfully run the migrations, have confirmed you have at least one table/row in the database, and made these changes; then you should be seeing the english post content in your index/index view.

That’s fine for the English site, but what about the other languages? We don’t want to have to add additional logic to determine which fields to show. We can’t use the translation layer for this either, because the translated phrases are in the database.

The answer is to modify the Post model:

public function getTitleAttribute()
{
$locale = App::getLocale();
$column = "title_" . $locale;
return $this->{$column};
}
public function getContentAttribute()
{
$locale = App::getLocale();
$column = "content_" . $locale;
return $this->{$column};
}
This was extracted from app/models/Post.php for brevity.

We’ve seen these attribute accessors before. They allow us to intercept calls to $post->title and $post->content, and provide our own return values. In this case; we return the locale-specific field value. Naturally we can adjust the view use:

<h1>
{{ $post->title }}
</h1>
<p>
{{ $post->content }}
</p>
This was extracted from app/views/www/index/index.blade.php for brevity.

We can use this in all the domain-specific views, to render locale-specific database data.

Conclusion

Laravel 4 is packed with excellent tools to build massive, multi-language, multi-domain applications. In addition, it also has an excellent ORM, template language, input validator and filter system. And that’s just the tip of the iceberg!

If you found this tutorial helpful, please tell me about it @followchrisp and be sure to recommend it to PHP developers looking to Laravel 4!

This tutorial comes from a book I’m writing. If you like it and want to support future tutorials; please consider buying it. Half of all sales go to Laravel.