Книга: Laravel 4 Cookbook
Назад: Access Control List
Дальше: API

Deployment

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.

The code for this chapter can be found at: https://github.com/formativ/tutorial-laravel-4-deployment

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.

Dependencies

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" 

This was extracted from composer.json.

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" 

This was extracted from composer.json.

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.

Environment Commands

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:

  1. The files contained in the root of app/config are merged or overridden by environment-based configuration files.
  2. Configuration files that are specific to an environment are stored in folders matching their environment name.
  3. Environments are determined by an array specified in bootstrap/start.php and are matched according to the name of the machine on which the application is being run.
  4. An application can have any number of environments; each with their own configuration files. There can also be multiple machine names (hosts) in each environment. You can have two staging servers, a production server and a testing server (for instance). If their machine names match those in bootstrap/start.php then their individual configuration files will be loaded.
  5. All Artisan commands can be given an –env option which will override the environment settings of the machine on which the commands are run. I’m sure there are other ways in which environments affect application execution, but you get the point: environments are a big-little thing.

Checking Environments

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 } 

This file should be saved as app/commands/FooCommand.php.

There are a few things of importance here:

  1. The $name property is used both to describe the command as well as invoke it. If we change it to foo, and register it correctly (as we’ll do in a moment), then we would be able to call it with: php artisan foo
  2. The description property is only descriptive. When all the registered command are displayed (by a call to: php artisan) then this description will be shown next to the name of the command.
  3. The fire() method is where all the action happens. If you want your command to do anything; there is where it needs to get done.
  4. The getArguments() method should return an array of arguments (or parameters) to the command. If our command was: php artisan foo bar, then bar would be an argument. Arguments are named (which we will see shortly).
  5. The getOptions() method should return an array of options (or flags) to the command. If out command was: php artisan foo –baz, then –baz would be an option. Options are also named (which we will also see shortly). This file gives us a good starting point from which to build our own set of commands.

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 } 

This file should be saved as app/commands/EnvironmentCommand.php.

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.

You may have noticed the XML notation within the calls to the line() method. Laravel 4’s console library extends Symphony 2’s console library; and Symphony 2’s console library allows these definitions in order to alter the meaning and appearance of text rendered to the console.

While the appearance will be changed, by using this notation, it’s not the best thing to be doing (semantically speaking). The bits in the comment elements aren’t comments any more than the bit in the info elements are.

We’re simply using it for a refreshing variation in the console text colours. If this sort of jacky behaviour offends your senses, feel free to omit the XML notation altogether!

Before we can run any commands; we need to register them with Artisan:

1 Artisan::add(new EnvironmentCommand); 

This was extracted from app/start/artisan.php.

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 } 

This file should be saved as app/commands/EnvironmentGetCommand.php.

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 } 

This was extracted from app/commands/EnvironmentCommand.php.

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 

Setting Environments

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 } 

This file should be saved as app/commands/EnvironmentSetCommand.php.

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\nreturn [";  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 } 

This was extracted from app/commands/EnvironmentCommand.php.

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 was extracted from bootstrap/start.php.

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.

Unsetting Environments

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 } 

This file should be saved as app/commands/EnvironmentRemoveCommand.php.

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.

Commands make up a lot of this tutorial. It may, therefore, surprise you to know that there’s not much more to them than this. Sure, there are different types (and values) of InputOption’s and InputArgument’s, but we’re not going into that level of detail here.

You can find out more about these at: http://symfony.com/doc/current/components/console/introduction.html

Asset Commands

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 ]; 

This file should be saved as app/config/local/asset.php.

 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 ]; 

This file should be saved as app/config/production/asset.php.

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 file should be saved as app/macros.php.

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"; 

This was extracted from app/start/global.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> 

This file should be saved as app/views/hello.blade.php.

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.

You will notice the difference by changing your environment from local to production. Give the environment commands a go — this is the kind of thing they were made for!

Combining Assets

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 } 

This file should be saved as app/commands/AssetCommand.php.

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 } 

This file should be saved as app/commands/AssetCombineCommand.php.

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.

You can learn more about Assetic at: https://github.com/kriswallsmith/assetic/

Minifying Assets

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 } 

This file should be saved as app/commands/AssetMinifyCommand.php.

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.

You can learn more about Assetic-Minifier at: https://github.com/toopay/assetic-minifier

Building Assets

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 } 

This file should be saved as app/commands/BuildCommand.php.

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.

Watching Assets

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 ] 

This was extracted from app/config/app.php.

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 } 

This file should be saved as app/commands/WatchCommand.php.

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.

You have to have the watcher running in order for updates to cascade into combined/minified asset files. Do this by running: php artisan watch

You can learn more about the Resource Watcher library at: https://github.com/jasonlewis/resource-watcher

Resource Watcher Integration Bug

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 } 

This file should be saved as vendor/jasonlewis/resource-watcher/src/JasonLewis/ResourceWatcher/Integration/LaravelServiceProvider.php.

Rsync

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. Back up any keys you already have in ~/.ssh
  2. Generate a new key with: ssh-keygen -t rsa -C “[email protected]
  3. Remember the name you set here.
  4. Copy the contents of the new public key file (the name from step 3, ending in .pub).
  5. SSH into your remove server.
  6. Add the contents of the new public key file (which you copied in step 3) to ~/.ssh/authorized_keys. Add the following lines to ~/.ssh/config (on your local machine):
1 host example.com 2     User your_user 3     IdentityFile your_key_file 

This was extracted from ~/.ssh/config.

The your_user account needs to be the same as the one with which you SSH’d into the remote server and added the authorised key.

The your_identity_file is the name from step 3 (not ending in .pub).

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…

Distribute Command

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.

Copying Files For Distribution

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 } 

This file should be saved as app/commands/CopyCommand.php.

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.

It’s important that you copy the distribution files to a target outside of your source folder. The copy command copies ./ which means a target inside will lead to an “infinite copy loop” where the command tries to copy the distribution folder into itself an infinite number of times.

To get around this; I guess you could target sources files individually (or in folders). It’s likely that you will be running the copy command from a local machine, so there’s little harm in copying the distribution files into a directory outside of the one your application files are in.

Removing Development Files

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 ]; 

This file should be saved as app/config/clean.php.

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 } 

This file should be saved as app/commands/CleanCommand.php.

The CleanCommand class gets all files matching the patterns in app/config/clean.php and deletes them. It handles folders as well.

Be very careful when deleting files. It’s always advisable to make a full backup before you test these kinds of scripts. I nearly nuked all the tutorial code because I didn’t make a backup!

Synchronising Files To A Remote Server

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 ]; 

This file should be saved as app/config/host.php.

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 } 

This file should be saved as app/commands/DistributeCommand.php.

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.

The intricacies of SSH and Rsync are outside the scope of this tutorial. You can learn more about Rsync at: https://calomel.org/rsync_tips.html

You can watch the progress of an upload (to the remote server) by connecting via SSH, navigating to the folder and running the command: du -hs

Command Portability

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

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.

Images

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!

Назад: Access Control List
Дальше: API