If you’ve ever had to build an application that shares some business logic, and even interface logic, across multiple interfaces then you will undoubtedly have debated the merits of instead creating multiple applications or finding some way to handle each interface within the same codebase.
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 chapter — 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.
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 int he first place. It’s not something I consider particularly tricky, and the internet is filled with wonderful tutorials on the subject.
I use it in this chapter, but I don’t really speak it. Google Translate does. Blame Google Translate.
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.
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.
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:
1
sudo vim /etc/hosts
On a new line, at the bottom of /etc/hosts, add:
1
127.0.0.1 dev.tutorial
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.
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.
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:
1
<VirtualHost
*:80
>
2
DocumentRoot /var/www/site1 3
ServerName dev.site1 4
</VirtualHost>
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.
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.
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:
1
server {
2
listen 80;
3
server_name dev.site1;
4
root /var/www/site1;
5
index index.php;
6
7
location ~ \.
php$
{
8
try_files $uri
=
404;
9
fastcgi_split_path_info ^(
.+\.
php)(
/.+)
$;
10
fastcgi_pass unix:/var/run/php5-fpm.sock;
11
fastcgi_index index.php 12
include fastcgi_params;
13
}
14
}
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:
1
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.
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.
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:
1
127.0.0.1 dev.www.tutorial-laravel-4-multisites 2
127.0.0.1 dev.admin.tutorial-laravel-4-multisites
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:
1
$env
=
$app
->
detectEnvironment
([
2
"www"
=>
[
"dev.www.tutorial-laravel-4-multisites"
],
3
"admin"
=>
[
"dev.admin.tutorial-laravel-4-multisites"
]
4
]);
If you would rather determine the current environment via a server configuration property; you can pass a callback to the detectEnvironment() method:
1
$env
=
$app
->
detectEnvironment
(
function
()
2
{
3
return
Input
::
server
(
"environment"
,
"development"
);
4
});
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.
1
<!doctype html>
2
<html
lang=
"en"
>
3
<head>
4
<meta
charset=
"utf-8"
/>
5
<title>
Laravel 4 Multisites</title>
6
</head>
7
<body>
8
@yield("content") 9
</body>
10
</html>
1
@extends("layout") 2
@section("content") 3
Welcome to our website! 4
@stop
1
<!doctype html>
2
<html
lang=
"en"
>
3
<head>
4
<meta
charset=
"utf-8"
/>
5
<title>
Laravel 4 Multisites — Admin</title>
6
</head>
7
<body>
8
@yield("content") 9
</body>
10
</html>
1
@extends("layout") 2
@section("content") 3
Please log in to use the admin. 4
@stop
Let’s also prepare the routes and controllers for the rest of the tutorial, by updating both:
1
<?
php
2
3
Route
::
any
(
"/"
,
[
4
"as"
=>
"index/index"
,
5
"uses"
=>
"IndexController@indexAction"
6
]);
1
<?
php
2
3
class
IndexController
4
extends
BaseController
5
{
6
public
function
indexAction
()
7
{
8
return
View
::
make
(
"index/index"
);
9
}
10
}
In order for Laravel to know which views to use for each environment, we should also create the configuration files for the environments.
1
<?
php
2
3
return
[
4
"paths"
=>
[
app_path
()
.
"/views/www"
]
5
];
1
<?
php
2
3
return
[
4
"paths"
=>
[
app_path
()
.
"/views/admin"
]
5
];
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…
Another aspect to multiple-domain applications is how they can affect (and be affected by) the routes file. Consider the following route groups:
1
Route
::
group
([
2
"domain"
=>
"dev.www.tutorial-laravel-4-multisites"
3
],
function
()
4
{
5
Route
::
any
(
"/about"
,
function
()
6
{
7
return
"This is the client-facing website."
;
8
});
9
});
10
11
Route
::
group
([
12
"domain"
=>
"dev.admin.tutorial-laravel-4-multisites"
13
],
function
()
14
{
15
Route
::
any
(
"/about"
,
function
()
16
{
17
return
"This is the admin site."
;
18
});
19
});
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:
1
Route
::
group
([
2
"domain"
=>
"dev.{sub}.tutorial-laravel-4-multisites"
3
],
function
()
4
{
5
Route
::
any
(
"/whoami"
,
function
(
$sub
)
6
{
7
return
"You are in the '"
.
$sub
.
"' sub-domain."
;
8
});
9
});
This functionality allows some pretty powerful domain-related configuration and logical branching!
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.
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:
1
<?
php
2
3
return
[
4
"instructions"
=>
"Follow these steps to operate cheese:"
,
5
"step1"
=>
"Cut the cheese."
,
6
"step2"
=>
"Eat the :product!"
,
7
"product"
=>
"cheese"
8
];
1
<?
php
2
3
return
[
4
"instructions"
=>
"Volg deze stappen om kaas te bedienen:"
,
5
"step1"
=>
"Snijd de kaas."
,
6
"step2"
=>
"Eet de :product!"
,
7
"product"
=>
"kaas"
8
];
1
<?
php
2
3
class
IndexController
4
extends
BaseController
5
{
6
public
function
indexAction
()
7
{
8
App
::
setLocale
(
"en"
);
9
10
if
(
Input
::
get
(
"lang"
)
===
"nl"
)
11
{
12
App
::
setLocale
(
"nl"
);
13
}
14
15
return
View
::
make
(
"index/index"
);
16
}
17
}
This will toggle the language based on a querystring parameter. The next step is actually using the phrases in views:
1
@extends("layout") 2
@section("content") 3
<h1>
4
{{ Lang::get("steps.instructions") }} 5
</h1>
6
<ol>
7
<li>
8
{{ trans("steps.step1") }} 9
</li>
10
<li>
11
{{ trans("steps.step2", [ 12
"product" => trans("steps.product") 13
]) }} 14
</li>
15
</ol>
16
@stop
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 :product placeholder. This allows the insertion of variable data into translation phrases. We can pass these in the optional second parameter of Lang::get() and trans().
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:
1
public
function
boot
()
2
{
3
$this
->
package
(
"formativ/multisite"
,
"multisite"
);
4
}
…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:
1
<?
php
2
3
return
[
4
"instructions"
=>
"Do these:"
5
];
1
<?
php
2
3
return
[
4
"instructions"
=>
"Hebben deze:"
5
];
Finally, let’s adjust the view to reflect the new translation phrase’s location:
1
<h1>
2
{{ Lang::get("multisite::steps.instructions") }} 3
</h1>
It’s as easy at that!
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:
1
// 'Lang' => 'Illuminate\Support\Facades\Lang',
2
"Lang"
=>
"Formativ\Multisite\Facades\Lang"
,
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…
1
<?
php
2
3
namespace
Formativ\Multisite\Facades
;
4
5
use
Illuminate\Support\Facades\Facade
;
6
7
class
Lang
8
extends
Facade
9
{
10
protected
static
function
getFacadeAccessor
()
11
{
12
return
"multisite.translator"
;
13
}
14
}
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:
1
public
function
register
()
2
{
3
$this
->
app
[
"multisite.translator"
]
=
4
$this
->
app
->
share
(
function
(
$app
)
5
{
6
$loader
=
$app
[
"translation.loader"
];
7
$locale
=
$app
[
"config"
][
"app.locale"
];
8
$trans
=
new
Translator
(
$loader
,
$locale
);
9
10
return
$trans
;
11
});
12
}
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.
1
<?
php
2
3
namespace
Formativ\Multisite
;
4
5
use
Cache
;
6
use
Illuminate\Translation\Translator
as
Original
;
7
8
class
Translator
9
extends
Original
10
{
11
public
function
get
(
$key
,
array
$replace
=
array
(),
12
$locale
=
null
)
13
{
14
$cached
=
Cache
::
remember
(
$key
,
15
,
15
function
()
use
(
$key
,
$replace
,
$locale
)
16
{
17
return
parent
::
get
(
$key
,
$replace
,
$local
18
});
19
20
return
$cached
;
21
}
22
}
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.
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:
1
<?
php
2
3
return
[
4
"cheese"
=>
"cheese"
,
5
"create"
=>
"create"
,
6
"update"
=>
"update"
,
7
"delete"
=>
"delete"
8
];
1
<?
php
2
3
return
[
4
"cheese"
=>
"kaas"
,
5
"create"
=>
"creëren"
,
6
"update"
=>
"bijwerken"
,
7
"delete"
=>
"verwijderen"
8
];
Next, we need to modify the app/routes.php file to dynamically create the routes:
1
$locales
=
[
2
"en"
,
3
"nl"
4
];
5
6
foreach
(
$locales
as
$locale
)
7
{
8
App
::
setLocale
(
$locale
);
9
10
$cheese
=
trans
(
"routes.cheese"
);
11
$create
=
trans
(
"routes.create"
);
12
$update
=
trans
(
"routes.update"
);
13
$delete
=
trans
(
"routes.delete"
);
14
15
Route
::
any
(
$cheese
.
"/"
.
$create
,
16
function
()
use
(
$cheese
,
$create
)
17
{
18
return
$cheese
.
"/"
.
$create
;
19
});
20
21
Route
::
any
(
$cheese
.
"/"
.
$update
,
22
function
()
use
(
$cheese
,
$update
)
23
{
24
return
$cheese
.
"/"
.
$update
;
25
});
26
27
Route
::
any
(
$cheese
.
"/"
.
$delete
,
28
function
()
use
(
$cheese
,
$delete
)
29
{
30
return
$cheese
.
"/"
.
$delete
;
31
});
32
}
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:
1
php artisan routes
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:
1
<?
php
2
3
use
Illuminate\Database\Migrations\Migration
;
4
5
class
CreatePostTable
6
extends
Migration
7
{
8
public
function
up
()
9
{
10
Schema
::
create
(
"post"
,
function
(
$table
)
11
{
12
$table
->
increments
(
"id"
);
13
$table
->
string
(
"title_en"
);
14
$table
->
string
(
"title_nl"
);
15
$table
->
text
(
"content_en"
);
16
$table
->
text
(
"content_nl"
);
17
$table
->
timestamps
();
18
});
19
}
20
public
function
down
()
21
{
22
Schema
::
dropIfExists
(
"post"
);
23
}
24
}
1
<?
php
2
3
class
DatabaseSeeder
4
extends
Seeder
5
{
6
public
function
run
()
7
{
8
Eloquent
::
unguard
();
9
$this
->
call
(
"PostTableSeeder"
);
10
}
11
}
1
<?
php
2
3
class
PostTableSeeder
4
extends
DatabaseSeeder
5
{
6
public
function
run
()
7
{
8
$posts
=
[
9
[
10
"title_en"
=>
"Cheese is the best"
,
11
"title_nl"
=>
"Kaas is de beste"
,
12
"content_en"
=>
"Research has shown..."
,
13
"content_nl"
=>
"Onderzoek heeft aangetoond..."
14
]
15
];
16
DB
::
table
(
"post"
)
->
insert
(
$posts
);
17
}
18
}
To get all of this in the database, we need to check the settings in app/config/database.php and run the following command:
1
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:
1
<?
php
2
3
class
Post
4
extends
Eloquent
5
{
6
protected
$table
=
"post"
;
7
}
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:
1
<?
php
2
3
class
IndexController
4
extends
BaseController
5
{
6
public
function
indexAction
(
$sub
)
7
{
8
App
::
setLocale
(
$sub
);
9
return
View
::
make
(
"index/index"
,
[
10
"post"
=>
Post
::
first
()
11
]);
12
}
13
}
Next, update the index/index.blade.php template:
1
<h1>
2
{{ $post->title_en }} 3
</h1>
4
<p>
5
{{ $post->content_en }} 6
</p>
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:
1
public
function
getTitleAttribute
()
2
{
3
$locale
=
App
::
getLocale
();
4
$column
=
"title_"
.
$locale
;
5
return
$this
->
{
$column
};
6
}
7
8
public
function
getContentAttribute
()
9
{
10
$locale
=
App
::
getLocale
();
11
$column
=
"content_"
.
$locale
;
12
return
$this
->
{
$column
};
13
}
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:
1
<h1>
2
{{ $post->title }} 3
</h1>
4
<p>
5
{{ $post->content }} 6
</p>
We can use this in all the domain-specific views, to render locale-specific database data.