There are few things which have improved my life quite as much as learning how to create a custom deployment process for my projects. Nothing is worse than having to worry about how to get your files onto a remote server, when you’ve got an important bug to fix.
Deployment processes are one of the most subjective things about development. Everyone’s got their own ideas about what should and shouldn’t be done. There are sometimes best practises; though these tend only to apply to subsets of the whole process.
Things get even more tricky when it comes to deploying Laravel 4 applications because there aren’t really any best practises to speak of, when it comes to working with remote servers and deploying code.
Remember this as you continue — this tutorial isn’t the only approach to deploying Laravel 4 applications. It’s simply a process I’ve found works for me.
Our deployment processes will need to handle JavaScript and CSS files. Assetic is an asset management library which we will use to do all the heavy lifting in this area.
To install it; we need to add two requirements to our composer.json file:
1
"kriswallsmith/assetic"
:
"1.2.*@dev"
,
2
"toopay/assetic-minifier"
:
"dev-master"
Lastly, we will also be using Jason Lewis’ Resource Watcher library, which integrates with Laravel 4’s Filesystem classes to enable notification of file changes. You’ll see why that’s useful in a bit…
1
"jasonlewis/resource-watcher"
:
"dev-master"
Once these requirements are added to the composer.json file, we need to update the vendor folder:
1
composer update
We’ll look at how to integrate these into our deployment workflow shortly.
Environments are a small, yet powerful, aspect of any Laravel 4 application. They primarily allow the specification of machine-based configuration options.
There are a few things you need to know about environments, in Laravel 4:
As I mentioned earlier; environments are usually specified in boostrap/start.php. This is probably going to be ok for the 99% of Laravel 4 applications that will ever be made, but we can improve upon it slightly still.
We’re going to make the list of environments somewhat dynamic, and load them in a slightly different way to how they are loaded out-the-box.
The first thing we’re going to do is learn how to make a command to tell us what the current environment is. Commands are the Laravel 4 way of extending the power and functionality of the Artisan command line tool. There are some commands already available when installing Laravel 4 (and we’ve seen some of them already, in previous tutorials).
To make our own, we can use an Artisan command:
1
php artisan command
:make FooCommand
This command will make a new file at app/commands/FooCommand .php. Inside this file you will begin to see what commands look like under the hood. Here’s an example of the file that gets generated:
1
<?
php
2
3
use
Illuminate\Console\Command
;
4
use
Symfony\Component\Console\Input\InputOption
;
5
use
Symfony\Component\Console\Input\InputArgument
;
6
7
class
FooCommand
extends
Command
{
8
9
/**
10
* The console command name.
11
*
12
* @var string
13
*/
14
protected
$name
=
'command:name'
;
15
16
/**
17
* The console command description.
18
*
19
* @var string
20
*/
21
protected
$description
=
'Command description.'
;
22
23
/**
24
* Create a new command instance.
25
*
26
* @return void
27
*/
28
public
function
__construct
()
29
{
30
parent
::
__construct
();
31
}
32
33
/**
34
* Execute the console command.
35
*
36
* @return void
37
*/
38
public
function
fire
()
39
{
40
//
41
}
42
43
/**
44
* Get the console command arguments.
45
*
46
* @return array
47
*/
48
protected
function
getArguments
()
49
{
50
return
array
(
51
array
(
52
'example'
,
53
InputArgument
::
REQUIRED
,
54
'An example argument.'
55
),
56
);
57
}
58
59
/**
60
* Get the console command options.
61
*
62
* @return array
63
*/
64
protected
function
getOptions
()
65
{
66
return
array
(
67
array
(
68
'example'
,
69
null
,
70
InputOption
::
VALUE_OPTIONAL
,
71
'An example option.'
,
72
null
73
),
74
);
75
}
76
77
}
There are a few things of importance here:
We begin our commands by creating the EnvironmentCommand class:
1
<?
php
2
3
use
Illuminate\Console\Command
;
4
5
class
EnvironmentCommand
6
extends
Command
7
{
8
protected
$name
=
"environment"
;
9
10
protected
$description
=
"Lists environment commands."
;
11
12
public
function
fire
()
13
{
14
$this
->
line
(
trim
(
"
15
<comment>environment:get</comment>
16
<info>gets host and environment.</info>
17
"
));
18
19
$this
->
line
(
trim
(
"
20
<comment>environment:set</comment>
21
<info>adds host to environment.</info>
22
"
));
23
24
$this
->
line
(
trim
(
"
25
<comment>environment:remove</comment>
26
<info>removes host from environment.</info>
27
"
));
28
}
29
30
protected
function
getArguments
()
31
{
32
return
[];
33
}
34
35
protected
function
getOptions
()
36
{
37
return
[];
38
}
39
}
The command class begins with us setting a useful name and description for the following commands we will create. The fire() method includes three calls to the line() method; which prints text to the command line. The getArguments() and getOptions() methods return empty arrays because we do not expect to handle any arguments or options.
Before we can run any commands; we need to register them with Artisan:
1
Artisan
::
add
(
new
EnvironmentCommand
);
There’s nothing much more to say than; this code registers the command with Artisan, so that it can be invoked. We should be able to call the command now, and it should show us something like the following:
1
❯ php artisan environment 2
environment:get gets host and environment. 3
environment:set adds host to environment. 4
environment:remove removes host from environment.
Congratulations! We’ve successfully created and registered our first Artisan command. Let’s go ahead and make a few more.
To make the first described command (environment:get); we’re going to subclass the EnvironmentCommand class we just created. We’ll be adding reusable code in the EnvironmentCommand class so it’s a means of accessing this code in related commands.
1
<?
php
2
3
use
Illuminate\Console\Command
;
4
5
class
EnvironmentGetCommand
6
extends
EnvironmentCommand
7
{
8
protected
$name
=
"environment:get"
;
9
10
protected
$description
=
"Gets host and environment."
;
11
12
public
function
fire
()
13
{
14
$this
->
line
(
trim
(
"
15
<comment>Host:</comment>
16
<info>"
.
$this
->
getHost
()
.
"</info>
17
"
));
18
19
$this
->
line
(
trim
(
"
20
<comment>Environment:</comment>
21
<info>"
.
$this
->
getEnvironment
()
.
"</info>
22
"
));
23
}
24
}
The EnvironmentGetCommand does slightly more than the previous command we made. It fetches the host name and the environment name from functions we must define in the EnvironmentCommand class:
1
protected
function
getHost
()
2
{
3
return
gethostname
();
4
}
5
6
protected
function
getEnvironment
()
7
{
8
return
App
::
environment
();
9
}
The gethostname() function returns the name of the machine on which it is invoked. Similarly; the App::environment() method returns the name of the environment in which Laravel is being run.
After registering and running this command, I see the following:
1
❯ php artisan environment:get 2
Host: formativ.local 3
Environment: local
The next command is going to allow us to alter these values from the command line (without changing hard-coded values)…
1
<?
php
2
3
use
Illuminate\Console\Command
;
4
use
Symfony\Component\Console\Input\InputArgument
;
5
use
Symfony\Component\Console\Input\InputOption
;
6
7
class
EnvironmentSetCommand
8
extends
EnvironmentCommand
9
{
10
protected
$name
=
"environment:set"
;
11
12
protected
$description
=
"Adds host to environment."
;
13
14
public
function
fire
()
15
{
16
$host
=
$this
->
getHost
();
17
$config
=
$this
->
getConfig
();
18
$overwrite
=
$this
->
option
(
"host"
);
19
$environment
=
$this
->
argument
(
"environment"
);
20
21
if
(
!
isset
(
$config
[
$environment
]))
22
{
23
$config
[
$environment
]
=
[];
24
}
25
26
$use
=
$host
;
27
28
if
(
$overwrite
)
29
{
30
$use
=
$overwrite
;
31
}
32
33
if
(
!
in_array
(
$use
,
$config
[
$environment
]))
34
{
35
$config
[
$environment
][]
=
$use
;
36
}
37
38
$this
->
setConfig
(
$config
);
39
40
$this
->
line
(
trim
(
"
41
<info>Added</info>
42
<comment>"
.
$use
.
"</comment>
43
<info>to</info>
44
<comment>"
.
$environment
.
"</comment>
45
<info>environment.</info>
46
"
));
47
}
48
49
protected
function
getArguments
()
50
{
51
return
[
52
[
53
"environment"
,
54
InputArgument
::
REQUIRED
,
55
"Environment to add the host to."
56
]
57
];
58
}
59
60
protected
function
getOptions
()
61
{
62
return
[
63
[
64
"host"
,
65
null
,
66
InputOption
::
VALUE_OPTIONAL
,
67
"Host to add."
,
68
null
69
]
70
];
71
}
72
}
The EnvironmentSetCommand class’ fire() method begins by getting the host name and a configuration array (using inherited methods). It also checks for a host option and an environment argument.
If the host option is provided; it will be added to the list of hosts for the provided environment. If no host option is provided; it will default to the machine the code is being executed on.
We also need to add the inherited methods to the EnvironmentCommand class:
1
protected
function
getHost
()
2
{
3
return
gethostname
();
4
}
5
6
protected
function
getEnvironment
()
7
{
8
return
App
::
environment
();
9
}
10
11
protected
function
getPath
()
12
{
13
return
app_path
()
.
"/config/environment.php"
;
14
}
15
16
protected
function
getConfig
()
17
{
18
$environments
=
require
$this
->
getPath
();
19
20
if
(
!
is_array
(
$environments
))
21
{
22
$environments
=
[];
23
}
24
25
return
$environments
;
26
}
27
28
protected
function
setConfig
(
$config
)
29
{
30
$config
=
"<?php return "
.
var_export
(
$config
,
true
)
.
";"
;
31
File
::
put
(
$this
->
getPath
(),
$config
);
32
}
This was extracted from app/commands/EnvironmentCommand.php.
The getConfig() method fetches the contents of the app/config/environment.php file (a list of hosts per environment) and the setConfig() method writes back to it. We use the var_export() to re-create the array that’s stored in memory; but it’s possible to get a more aesthetically-pleasing configuration file. I’ve customised the setConfig() method to match my personal taste:
1
protected
function
setConfig
(
$config
)
2
{
3
4
$code
=
"<?php
\n\n
return ["
;
5
6
foreach
(
$config
as
$environment
=>
$hosts
)
7
{
8
$code
.=
"
\n
\"
"
.
$environment
.
"
\"
=> ["
;
9
10
foreach
(
$hosts
as
$host
)
11
{
12
$code
.=
"
\n
\"
"
.
$host
.
"
\"
,"
;
13
}
14
15
$code
=
trim
(
$code
,
","
);
16
$code
.=
"
\n
],"
;
17
}
18
19
$code
=
trim
(
$code
,
","
);
20
File
::
put
(
$this
->
getPath
(),
$code
.
"
\n
];"
);
21
}
In order for these environments to be of any use to us; we need to replace those defined in bootstrap/start.php with the following lines:
1
$env
=
$app
->
detectEnvironment
(
2
require
__DIR__
.
"/../app/config/environment.php"
3
);
This ensures that the environments we set (using our environment commands), and not those hard-coded in the bootstrap/start.php file are used in determining the current machine environment.
The last environment command we will create will provide us a way to remove hosts from an environment (in much the same way as they were added):
1
<?
php
2
3
use
Illuminate\Console\Command
;
4
use
Symfony\Component\Console\Input\InputArgument
;
5
use
Symfony\Component\Console\Input\InputOption
;
6
7
class
EnvironmentRemoveCommand
8
extends
EnvironmentCommand
9
{
10
protected
$name
=
"environment:remove"
;
11
12
protected
$description
=
"Removes host from environment."
;
13
14
public
function
fire
()
15
{
16
$host
=
$this
->
getHost
();
17
$config
=
$this
->
getConfig
();
18
$overwrite
=
$this
->
option
(
"host"
);
19
$environment
=
$this
->
argument
(
"environment"
);
20
21
if
(
!
isset
(
$config
[
$environment
]))
22
{
23
$config
[
$environment
]
=
[];
24
}
25
26
$use
=
$host
;
27
28
if
(
$overwrite
)
29
{
30
$use
=
$overwrite
;
31
}
32
33
foreach
(
$config
[
$environment
]
as
$index
=>
$item
)
34
{
35
if
(
$item
==
$use
)
36
{
37
unset
(
$config
[
$environment
][
$index
]);
38
}
39
}
40
41
$this
->
setConfig
(
$config
);
42
43
$this
->
line
(
trim
(
"
44
<info>Removed</info>
45
<comment>"
.
$use
.
"</comment>
46
<info>from</info>
47
<comment>"
.
$environment
.
"</comment>
48
<info>environment.</info>
49
"
));
50
}
51
52
protected
function
getArguments
()
53
{
54
return
[
55
[
56
"environment"
,
57
InputArgument
::
REQUIRED
,
58
"Environment to remove the host from."
59
]
60
];
61
}
62
63
protected
function
getOptions
()
64
{
65
return
[
66
[
67
"host"
,
68
null
,
69
InputOption
::
VALUE_OPTIONAL
,
70
"Host to remove."
,
71
null
72
]
73
];
74
}
75
}
It’s pretty much the same as the EnvironmentSetCommand class, but instead of adding them to the configuration file; we remove them from the configuration file. It uses the same formatter as the EnvironmentSetCommand class.
There are two parts to managing assets. The first is how they are stored and referenced in views, and the second is how they are combined/minified.
Both of these operations will require a sane method of specifying asset containers and the assets contained therein. For this; we will create a new configuration file (in each environment we will be using):
1
<?
php
2
3
return
[
4
"header-css"
=>
[
5
"css/bootstrap.css"
,
6
"css/shared.css"
7
],
8
"footer-js"
=>
[
9
"js/jquery.js"
,
10
"js/bootstrap.js"
,
11
"js/shared.js"
12
]
13
];
1
<?php 2
3
return
[
4
"header-css"
=
> [
5
"css/shared.min.css"
=
> [
6
"css/bootstrap.css"
, 7
"css/shared.css"
8
]
9
]
, 10
"footer-js"
=
> [
11
"js/shared.min.js"
=
> [
12
"js/jquery.js"
, 13
"js/bootstrap.js"
, 14
"js/shared.js"
15
]
16
]
17
]
;
The difference between these environment-based asset configuration files is how the files are combined in the production environment vs. how they are simply listed in the local environment. This makes development easier because you can see unaltered files while still developing and testing; while the production environment will have smaller file sizes.
To use these in our views; we’re going to make a form macro. It’s not technically what form macros were made for (since we’re not using them to make forms) but it’s such a convenient/clean method for generating view markup that we’re going to use it anyway.
1
<?
php
2
3
Form
::
macro
(
"assets"
,
function
(
$section
)
4
{
5
$markup
=
""
;
6
$assets
=
Config
::
get
(
"asset"
);
7
8
if
(
isset
(
$assets
[
$section
]))
9
{
10
foreach
(
$assets
[
$section
]
as
$key
=>
$value
)
11
{
12
$use
=
$value
;
13
14
if
(
is_string
(
$key
))
15
{
16
$use
=
$key
;
17
}
18
19
if
(
ends_with
(
$use
,
".css"
))
20
{
21
$markup
.=
"<link
22
rel='stylesheet'
23
type='text/css'
24
href='"
.
asset
(
$use
)
.
"'
25
/>"
;
26
}
27
28
if
(
ends_with
(
$use
,
".js"
))
29
{
30
$markup
.=
"<script
31
type='text/javascript'
32
src='"
.
asset
(
$use
)
.
"'
33
></script>"
;
34
}
35
}
36
}
37
38
return
$markup
;
39
});
This macro accepts a single parameter — the name of the section — and renders HTML markup elements for each asset file. It doesn’t matter if there are a combination of stylesheets and scripts as the applicable tag is rendered for each asset type.
We also need to make sure this file gets included in our application’s startup processes:
1
require
app_path
()
.
"/macros.php"
;
We can now use this in our templates…
1
<!DOCTYPE html>
2
<html
lang=
"en"
>
3
<head>
4
<meta
charset=
"UTF-8"
/>
5
<title>
Laravel 4 — Deployment Tutorial</title>
6
{{ Form::assets("header-css") }} 7
</head>
8
<body>
9
<h1>
Hello World!</h1>
10
{{ Form::assets("footer-js") }} 11
</body>
12
</html>
Now, depending on the environment we have set; we will either see a list of asset files in each section, or single (production) asset files.
The simplest asset operation is combining. This is where we put two or more stylesheets or scripts together into a single file. Similarly to how we arranged the environment classes; the asset commands will inherit form a master command:
1
<?
php
2
3
use
Assetic\Asset\AssetCollection
;
4
use
Assetic\Asset\FileAsset
;
5
use
Illuminate\Console\Command
;
6
7
class
AssetCommand
8
extends
Command
9
{
10
protected
$name
=
"asset"
;
11
12
protected
$description
=
"Lists asset commands."
;
13
14
public
function
fire
()
15
{
16
$this
->
line
(
trim
(
"
17
<comment>asset:combine</comment>
18
<info>combines resource files.</info>
19
"
));
20
21
$this
->
line
(
trim
(
"
22
<comment>asset:minify</comment>
23
<info>minifies resource files.</info>
24
"
));
25
}
26
27
protected
function
getArguments
()
28
{
29
return
[];
30
}
31
32
protected
function
getOptions
()
33
{
34
return
[];
35
}
36
37
protected
function
getPath
()
38
{
39
return
public_path
();
40
}
41
42
protected
function
getCollection
(
$input
,
$filters
=
[])
43
{
44
$path
=
$this
->
getPath
();
45
$input
=
explode
(
","
,
$input
);
46
$collection
=
new
AssetCollection
([],
$filters
);
47
48
foreach
(
$input
as
$asset
)
49
{
50
$collection
->
add
(
51
new
FileAsset
(
$path
.
"/"
.
$asset
)
52
);
53
}
54
55
return
$collection
;
56
}
57
58
protected
function
setOutput
(
$file
,
$content
)
59
{
60
$path
=
$this
->
getPath
();
61
return
File
::
put
(
$path
.
"/"
.
$file
,
$content
);
62
}
63
}
Here’s where we make use of Assetic. Among the many utilities Assetic provides; the AssetCollection and FileAsset classes will be out main focus. The getCollection() method accepts a comma-delimited list of asset files (relative to the public folder) and returns a populated AssetCollection instance.
1
<?
php
2
3
use
Illuminate\Console\Command
;
4
use
Symfony\Component\Console\Input\InputArgument
;
5
use
Symfony\Component\Console\Input\InputOption
;
6
7
class
AssetCombineCommand
8
extends
AssetCommand
9
{
10
protected
$name
=
"asset:combine"
;
11
12
protected
$description
=
"Combines resource files."
;
13
14
public
function
fire
()
15
{
16
$input
=
$this
->
argument
(
"input"
);
17
$output
=
$this
->
option
(
"output"
);
18
$combined
=
$this
->
getCollection
(
$input
)
->
dump
();
19
20
if
(
$output
)
21
{
22
$this
->
line
(
trim
(
"
23
<info>Successfully combined</info>
24
<comment>"
.
$input
.
"</comment>
25
<info>to</info>
26
<comment>"
.
$output
.
"</comment>
27
<info>.</info>
28
"
));
29
30
$this
->
setOutput
(
$output
,
$combined
);
31
}
32
else
33
{
34
$this
->
line
(
$combined
);
35
}
36
}
37
38
protected
function
getArguments
()
39
{
40
return
[
41
[
42
"input"
,
43
InputArgument
::
REQUIRED
,
44
"Names of input files."
45
]
46
];
47
}
48
49
protected
function
getOptions
()
50
{
51
return
[
52
[
53
"output"
,
54
null
,
55
InputOption
::
VALUE_OPTIONAL
,
56
"Name of output file."
,
57
null
58
]
59
];
60
}
61
}
The AssetCombineCommand class expects the aforementioned list of assets and will output the results to console, by default. If we want to save the results to a file we can provide the –output option with a path to the combined file.
Minifying asset files is just as easy; thanks to the Assetic-Minifier filters. Assetic provides filters which can be used to transform the output of asset files.
Usually some of these filters depend on third-party software being installed on the server, or things like Java applets. Fortunately, the Assetic-Minifier library provides pure PHP alternatives to CssMin and JSMin filters (which would otherwise need additional software installed).
1
<?
php
2
3
use
Minifier\MinFilter
;
4
use
Illuminate\Console\Command
;
5
use
Symfony\Component\Console\Input\InputArgument
;
6
use
Symfony\Component\Console\Input\InputOption
;
7
8
class
AssetMinifyCommand
9
extends
AssetCommand
10
{
11
protected
$name
=
"asset:minify"
;
12
13
protected
$description
=
"Minifies resource files."
;
14
15
public
function
fire
()
16
{
17
$type
=
$this
->
argument
(
"type"
);
18
$input
=
$this
->
argument
(
"input"
);
19
$output
=
$this
->
option
(
"output"
);
20
$filters
=
[];
21
22
if
(
$type
==
"css"
)
23
{
24
$filters
[]
=
new
MinFilter
(
"css"
);
25
}
26
27
if
(
$type
==
"js"
)
28
{
29
$filters
[]
=
new
MinFilter
(
"js"
);
30
}
31
32
$collection
=
$this
->
getCollection
(
$input
,
$filters
);
33
$combined
=
$collection
->
dump
();
34
35
if
(
$output
)
36
{
37
$this
->
line
(
trim
(
"
38
<info>Successfully minified</info>
39
<comment>"
.
$input
.
"</comment>
40
<info>to</info>
41
<comment>"
.
$output
.
"</comment>
42
<info>.</info>
43
"
));
44
45
$this
->
setOutput
(
$output
,
$combined
);
46
}
47
else
48
{
49
$this
->
line
(
$combined
);
50
}
51
}
52
53
protected
function
getArguments
()
54
{
55
return
[
56
[
57
"type"
,
58
InputArgument
::
REQUIRED
,
59
"Code type."
60
],
61
[
62
"input"
,
63
InputArgument
::
REQUIRED
,
64
"Names of input files."
65
]
66
];
67
}
68
69
protected
function
getOptions
()
70
{
71
return
[
72
[
73
"output"
,
74
null
,
75
InputOption
::
VALUE_OPTIONAL
,
76
"Name of output file."
,
77
null
78
]
79
];
80
}
81
}
The minify command accepts two arguments — the type of assets (either css or js) and the comma-delimited list of asset files to minify. Like the combine command; it will output to console by default, unless the —output option is specified.
Commands that accept inputs and outputs are really useful for the combining and minifying asset files, but it’s a pain to have to specify inputs and outputs each time (especially when we have gone to the effort of defining asset lists in configuration files). For this purpose; we need a command which will combine and minify all the asset files appropriately.
1
<?
php
2
3
use
Illuminate\Console\Command
;
4
5
class
BuildCommand
6
extends
Command
7
{
8
protected
$name
=
"build"
;
9
10
protected
$description
=
"Builds resource files."
;
11
12
public
function
fire
()
13
{
14
$sections
=
Config
::
get
(
"asset"
);
15
16
foreach
(
$sections
as
$section
=>
$assets
)
17
{
18
foreach
(
$assets
as
$output
=>
$input
)
19
{
20
if
(
!
is_string
(
$output
))
21
{
22
continue
;
23
}
24
25
if
(
!
is_array
(
$input
))
26
{
27
$input
=
[
$input
];
28
}
29
30
$input
=
join
(
","
,
$input
);
31
32
$options
=
[
33
"--output"
=>
$output
,
34
"input"
=>
$input
35
];
36
37
if
(
ends_with
(
$output
,
".min.css"
))
38
{
39
$options
[
"type"
]
=
"css"
;
40
$this
->
call
(
"asset:minify"
,
$options
);
41
}
42
else
if
(
ends_with
(
$output
,
".min.js"
))
43
{
44
$options
[
"type"
]
=
"js"
;
45
$this
->
call
(
"asset:minify"
,
$options
);
46
}
47
else
48
{
49
$this
->
call
(
"asset:combine"
,
$options
);
50
}
51
}
52
}
53
}
54
}
The BuildCommand class fetches the asset lists, from the configuration files, and iterates over them; combining/minifying as needed. It does this by checking file extensions. If the file ends in .min.js then the minify command is run in js mode. Files ending in .min.css are minified in css mode, and so forth.
The app/config/*/asset.php files are environment-based. This means running the build command will build the assets for the current environment. Often this will not be the environment you want to build assets for. I often build assets on my local machine, yet I want assets built for production. When that is the case; I provide the –env=production flag to the build command.
So we have the tools to combine, minify and even build our asset files. It’s a pain to have to remember to do those things all the time (especially when files are being updated at a steady pace), so we’re going to take it a step further by adding a file watcher.
Remember the Resource Watcher library we added to composer.json? Well we need to add it to the list of service providers:
1
"providers"
=>
[
2
// other service providers here
3
"JasonLewis\ResourceWatcher\Integration\LaravelServiceProvider"
4
]
The Resource Watcher library requires Laravel 4’s Filesystem library, and this service provider will allow us to utilise dependency injection.
1
<?
php
2
3
use
Illuminate\Console\Command
;
4
5
class
WatchCommand
6
extends
Command
7
{
8
protected
$name
=
"watch"
;
9
10
protected
$description
=
"Watches for file changes."
;
11
12
public
function
fire
()
13
{
14
$path
=
$this
->
getPath
();
15
$watcher
=
App
::
make
(
"watcher"
);
16
$sections
=
Config
::
get
(
"asset"
);
17
18
foreach
(
$sections
as
$section
=>
$assets
)
19
{
20
foreach
(
$assets
as
$output
=>
$input
)
21
{
22
if
(
!
is_string
(
$output
))
23
{
24
continue
;
25
}
26
27
if
(
!
is_array
(
$input
))
28
{
29
$input
=
[
$input
];
30
}
31
32
foreach
(
$input
as
$file
)
33
{
34
$watch
=
$path
.
"/"
.
$file
;
35
$listener
=
$watcher
->
watch
(
$watch
);
36
37
$listener
->
onModify
(
function
()
use
(
38
$section
,
39
$output
,
40
$input
,
41
$file
42
)
43
{
44
$this
->
build
(
45
$section
,
46
$output
,
47
$input
,
48
$file
49
);
50
});
51
}
52
}
53
}
54
55
$watcher
->
startWatch
();
56
}
57
58
protected
function
build
(
$section
,
$output
,
$input
,
$file
)
59
{
60
$options
=
[
61
"--output"
=>
$output
,
62
"input"
=>
join
(
","
,
$input
)
63
];
64
65
$this
->
line
(
trim
(
"
66
<info>Rebuilding</info>
67
<comment>"
.
$output
.
"</comment>
68
<info>after change to</info>
69
<comment>"
.
$file
.
"</comment>
70
<info>.</info>
71
"
));
72
73
if
(
ends_with
(
$output
,
".min.css"
))
74
{
75
$options
[
"type"
]
=
"css"
;
76
$this
->
call
(
"asset:minify"
,
$options
);
77
}
78
else
if
(
ends_with
(
$output
,
".min.js"
))
79
{
80
$options
[
"type"
]
=
"js"
;
81
$this
->
call
(
"asset:minify"
,
$options
);
82
}
83
else
84
{
85
$this
->
call
(
"asset:combine"
,
$options
);
86
}
87
}
88
89
protected
function
getArguments
()
90
{
91
return
[];
92
}
93
94
protected
function
getOptions
()
95
{
96
return
[];
97
}
98
99
protected
function
getPath
()
100
{
101
return
public_path
();
102
}
103
}
The WatchCommand class is a step up from the BuildCommand class in that it processes asset files similarly. Where it excels is in how it is able to watch the individual files in app/config/*/asset.php.
When a Watcher targets a specific file (with the $watcher->watch() method); it generates a Listener. Listeners can have events bound to them (as is the case with the onModify() event listener method).
When these files change, the processed asset files they form part of are rebuilt, using the same logic as in the build command.
While creating this tutorial; I found a bug with the service provider. It relates to class resolution, and was probably caused by a reshuffling of the library. I have since contacted Jason Lewis to inform him of the bug, and submitted a pull request which resolves it.
If you are having issues relating to the Resource Watcher library, try replacing the service provider file with this one:
1
<?
php
namespace
JasonLewis\ResourceWatcher\Integration
;
2
3
use
Illuminate\Support\ServiceProvider
;
4
use
JasonLewis\ResourceWatcher\Tracker
;
5
use
JasonLewis\ResourceWatcher\Watcher
;
6
7
class
LaravelServiceProvider
extends
ServiceProvider
{
8
9
/**
10
* Indicates if loading of the provider is deferred.
11
*
12
* @var bool
13
*/
14
protected
$defer
=
false
;
15
16
/**
17
* Register the service provider.
18
*
19
* @return void
20
*/
21
public
function
register
()
22
{
23
$this
->
app
[
'watcher'
]
=
$this
->
app
->
share
(
function
(
$app
)
24
{
25
$tracker
=
new
Tracker
;
26
return
new
Watcher
(
$tracker
,
$app
[
'files'
]);
27
});
28
}
29
30
/**
31
* Get the services provided by the provider.
32
*
33
* @return array
34
*/
35
public
function
provides
()
36
{
37
return
array
(
'watcher'
);
38
}
39
}
Rsync is a file synchronisation utility which we will use to synchronise our distribution folder to a remote server. It requires a valid private/public key and some configuration.
To set up SSH access, for your domain, follow these steps:
1
host example.com 2
User your_user 3
IdentityFile your_key_file
Now, when you type ssh example.com (where example.com is the name of the domain you’ve been accessing with SSH); you should be let in without even having to provide a password. Don’t worry — your server is still secure. You’ve just let it know (ahead of time) what an authentic connection from you looks like.
With this configuration in place; you won’t need to do anything tricky in order to get Rsync to work correctly. The hard part is getting a good connection…
In order for us to distribute our code, we need to make a copy of it and perform some operations on the copy. This involves optimisation and cleanup.
First, let’s make the copy command:
1
<?
php
2
3
use
Illuminate\Console\Command
;
4
use
Symfony\Component\Console\Input\InputOption
;
5
6
class
CopyCommand
7
extends
Command
8
{
9
protected
$name
=
"copy"
;
10
11
protected
$description
=
"Creates distribution files."
;
12
13
public
function
fire
()
14
{
15
$target
=
$this
->
option
(
"target"
);
16
17
if
(
!
$target
)
18
{
19
$target
=
"../distribution"
;
20
}
21
22
File
::
copyDirectory
(
"./"
,
$target
);
23
24
$this
->
line
(
trim
(
"
25
<info>Successfully copied source files to</info>
26
<comment>"
.
realpath
(
$target
)
.
"</comment>
27
<info>.</info>
28
"
));
29
}
30
31
protected
function
getArguments
()
32
{
33
return
[];
34
}
35
36
protected
function
getOptions
()
37
{
38
return
[
39
[
40
"target"
,
41
null
,
42
InputOption
::
VALUE_OPTIONAL
,
43
"Distribution path."
,
44
null
45
]
46
];
47
}
48
}
The copy command uses Laravel 4’s File methods to copy the source directly recursively. It initially targets ../distribute but this can be changed with the –target option.
Next up, we need a command to remove any temporary/development files. We’ll start by creating a config file in which these files are listed:
1
<?
php
2
3
return
[
4
"app/storage/cache/*"
,
5
"app/storage/logs/*"
,
6
"app/storage/sessions/*"
,
7
"app/storage/views/*"
,
8
".DS_Store"
,
9
".git*"
,
10
".svn*"
,
11
"public/js/jquery.js"
,
12
"public/js/bootstrap.js"
,
13
"public/js/shared.js"
,
14
"public/css/bootstrap.css"
,
15
"public/css/shared.css"
16
];
I’ve just listed a few common temporary/development files. You can use any syntax that works with PHP’s glob() function, as this is what Laravel 4 is using behind the scenes.
I’m also removing the development scripts and styles, as these will be built into minified asset files by the build command.
1
<?
php
2
3
use
Illuminate\Console\Command
;
4
use
Symfony\Component\Console\Input\InputOption
;
5
6
class
CleanCommand
7
extends
Command
8
{
9
protected
$name
=
"clean"
;
10
11
protected
$description
=
"Cleans distribution folder."
;
12
13
public
function
fire
()
14
{
15
$target
=
$this
->
option
(
"target"
);
16
17
if
(
!
$target
)
18
{
19
$target
=
"../distribution"
;
20
}
21
22
$cleaned
=
Config
::
get
(
"clean"
);
23
24
foreach
(
$cleaned
as
$pattern
)
25
{
26
$paths
=
File
::
glob
(
$target
.
"/"
.
$pattern
);
27
28
foreach
(
$paths
as
$path
)
29
{
30
if
(
File
::
isFile
(
$path
))
31
{
32
File
::
delete
(
$path
);
33
}
34
35
if
(
File
::
isDirectory
(
$path
))
36
{
37
File
::
deleteDirectory
(
$path
);
38
}
39
}
40
41
$this
->
line
(
trim
(
"
42
<info>Deleted all files/folders matching</info>
43
<comment>"
.
$pattern
.
"</comment>
44
<info>.</info>
45
"
));
46
}
47
}
48
49
protected
function
getArguments
()
50
{
51
return
[];
52
}
53
54
protected
function
getOptions
()
55
{
56
return
[
57
[
58
"target"
,
59
null
,
60
InputOption
::
VALUE_OPTIONAL
,
61
"Distribution path."
,
62
null
63
]
64
];
65
}
66
}
The CleanCommand class gets all files matching the patterns in app/config/clean.php and deletes them. It handles folders as well.
We’ve set up SSH access and we have code ready for deployment. Before we sync, we should set up a config file for target remote servers:
1
<?
php
2
3
return
[
4
"production"
=>
[
5
"url"
=>
"example.com"
,
6
"path"
=>
"/var/www"
7
]
8
];
The normal SSH access we’ve set up takes care of the username and password, so all we need to be able to configure is the url of the remote server and the path on it to upload the files to.
1
<?
php
2
3
use
Illuminate\Console\Command
;
4
use
Symfony\Component\Console\Input\InputArgument
;
5
use
Symfony\Component\Console\Input\InputOption
;
6
7
class
DistributeCommand
8
extends
Command
9
{
10
protected
$name
=
"distribute"
;
11
12
protected
$description
=
"Synchronises files with target."
;
13
14
public
function
fire
()
15
{
16
$host
=
$this
->
argument
(
"host"
);
17
$target
=
$this
->
option
(
"target"
);
18
19
if
(
!
$target
)
20
{
21
$target
=
"../distribution"
;
22
}
23
24
$url
=
Config
::
get
(
"host."
.
$host
.
".url"
);
25
$path
=
Config
::
get
(
"host."
.
$host
.
".path"
);
26
27
$command
=
"rsync --verbose --progress --stats --compress --recursive --t\
28
imes --perms -e ssh "
.
$target
.
"/ "
.
$url
.
":"
.
$path
.
"/"
;
29
30
$escaped
=
escapeshellcmd
(
$command
);
31
32
$this
->
line
(
trim
(
"
33
<info>Synchronizing distribution files to</info>
34
<comment>"
.
$host
.
" ("
.
$url
.
")</comment>
35
<info>.</info
36
"
));
37
38
exec
(
$escaped
,
$output
);
39
40
foreach
(
$output
as
$line
)
41
{
42
if
(
starts_with
(
$line
,
"Number of files transferred"
))
43
{
44
$parts
=
explode
(
":"
,
$line
);
45
46
$this
->
line
(
trim
(
"
47
<comment>"
.
trim
(
$parts
[
1
])
.
"</comment>
48
<info>files transferred.</info>
49
"
));
50
}
51
52
if
(
starts_with
(
$line
,
"Total transferred file size"
))
53
{
54
$parts
=
explode
(
":"
,
$line
);
55
$this
->
line
(
trim
(
"
56
<comment>"
.
trim
(
$parts
[
1
])
.
"</comment>
57
<info>transferred.</info>
58
"
));
59
}
60
}
61
}
62
63
protected
function
getArguments
()
64
{
65
return
[
66
[
67
"host"
,
68
InputArgument
::
REQUIRED
,
69
"Destination host."
70
]
71
];
72
}
73
74
protected
function
getOptions
()
75
{
76
return
[
77
[
78
"target"
,
79
null
,
80
InputOption
::
VALUE_OPTIONAL
,
81
"Distribution path."
,
82
null
83
]
84
];
85
}
86
}
The DistributeCommand class accepts a host (remote server name) argument and a –target option. Like the copy and clean commands; the –target option will override the default ../distribute folder with one of your choosing.
The host argument needs to match a key in your app/config/host.php configuration file. It carefully constructs and escapes a shell command instruction rsync to synchronise files from the distribution folder to a path on the remote server.
Once the files have been synchronised, it will inspect the messy output of the rsync command and return number number of bytes and files transferred.
Portability refers to how easily code will run on any environment. Most of the commands created in this tutorial are portable. The parts that aren’t too portable are those relating to Rsync. Rsync is a *nix utility, and is therefore not found on Windows machines.
If you find the deploy command gives you headaches; feel free to jump in just before it and deploy the distribution folder (after build, copy and clean commands) by whatever means works for you.
Preprocessors are things which convert some intermediate languages into common languages (such as converting Less, SASS, Stylus into CSS). These are often combined with deployment workflows.
Due to time constraints; I’ve not covered them in this tutorial. If you need to add them then a good place would be as a filter in the asset:combine or asset:minify commands. You may also want to add another command (to be executed before those two) which preprocesses any of these languages.
Image optimisation is a huge topic. I might go into detail on how to optimise your images (as part of the deployment process) in a future tutorial. Assetic does provide filters for this sort of thing; so check its documentation if you feel up to the challenge!