The Laravel PHP framework offers its bundles system to allow developers to redistribute useful packages of code, or to organize applications into several "bundles" of smaller applications.
In this tutorial, we will learn the ins and outs of creating and distributing bundles from scratch.
A Laravel bundle has access to all of the features that the framework offers to its host application, including routing, migrations, tests, views and numerous other useful features.
Here's a little secret, between us: the
application
folder of the Laravel source package is also a bundle, which Laravel refers to as theDEFAULT_BUNDLE
.
When to Create a Bundle?
Before writing a new piece of code, I like to ask myself a few simple questions to determine whether it is appropriate for a bundle. Let me share this technique with you.
Could this code be useful to others?
If the answer to this question is yes, then I would first ensure that someone has not already created a similar bundle or package. Other than for learning purposes, it's pointless to recreate the wheel. If the other package is of a high enough standard to be used in your project, then use that instead and save yourself the time.
Secondly, I think about the code and decide whether or not it might be useful to users of other frameworks, or people not using a framework at all. If the code is not related to the Laravel framework and does not need to make use of Laravel's core classes, then I would create a Composer package instead. Composer packages are widely becoming the standard for sharing code that is not restricted to a single framework or project.
For more information on Composer, refer to the following links:
If the code could be useful to others, and is dependent upon the Laravel framework, then you have a good reason to create a new bundle.
Will I have to write this code again?
DRY is the name of the game.
If the code provides functionality that you write frequently, then it makes sense to create a bundle. DRY (Don't repeat yourself!) is the name of the game.
Could this code be considered a stand-alone application?
For example, you may be building a simple site that, amongst other features, has a blog component. The blog could be considered a separate application to be contained in a bundle for much greater organization of your project.
Another example would be an administrative section, or 'back-end' for your website. This section could easily be considered a separate component from the main application, and could instead be organized into one or more bundles.
Would this code fit into a single class?
If this is the case, you might consider writing a 'Library' instead. A library is a single class that contains reusable code. It can be added to a Laravel project easily by dropping the class into the application/libraries/
directory, which is auto loaded by default.
Creating a Bundle
Let's create a simple plug-in that interacts with the Gravatar service to offer a simple method for generating avatars of various sizes within our main application. We will also add the necessary functionality to enter an email address and avatar size, and preview the associated gravatar on the page.
Let's get started by creating a new directory within the /bundles
directory of our project. We will call the directory and our bundle gravvy
. Not gravy... gravvy.
Let's add gravvy to the bundles array within application/bundles.php
so that we can test it as we go along. We will add an 'auto' => true
option to the array so that the bundle will be started automatically, and any autoloader mappings we create will be available to the whole of Laravel.
return array( 'docs' => array('handles' => 'docs'), 'gravvy' => array( 'auto' => true ) );
First, we will need to create a small library that will retrieve a user's avatar, using an email address. Create a new file within the root of the bundle, named gravvy.php
. Let's create a class, called Gravvy
with a static method, make()
, to replicate the naming scheme used by Laravel's own libraries.
The make()
method will accept two parameters: an email address and an integer to represent the size of the avatar to retrieve.
<?php /** * Class to create Gravatar image elements. * * @author You <[email protected]> */ class Gravvy { /** * Create a new image element from an email address. * @param string $email The email address. * @param integer $size The avatar size. * @return string The source for an image element. */ public static function make($email, $size = 32) { // convert our email into an md5 hash $email = md5($email); // return the image element return '<img src="http://www.gravatar.com/avatar/' .$email.'?s='.$size; } }
Bundle root directories aren't auto-loaded, so let's write a mapping so that Laravel knows where to find the 'Gravvy' class when it needs it.
When starting a bundle, Laravel looks for a file, named start.php
, and executes it. So let's create one within our new bundle's directory to hold our auto-load mappings.
<?php // /bundles/gravvy/start.php Autoload::map(array( 'Gravvy' => path('bundles').'/gravvy/gravvy.php' ));
Now Laravel will knows where to find the definition for our Gravvy
class, and will load the source when it first needs it. Very efficient!
The path()
method is a helper function, which returns the absolute path to useful folders used by Laravel. In this case, we are using it to retrieve the absolute path to the bundles directory.
Now that the we have our working Gravvy class, we could attempt to use it from within a controller to see if we get the expected output, but I think it would be more appropriate to write a unit test.
Just like the host application, unit tests are available from within the bundle. Let's create a tests
folder within the bundle, and add a new file, called general.test.php
.
<?php class TestGeneral extends PHPUnit_Framework_TestCase { /** * Test that an avatars output appears as expected. * * @return void */ public function testAvatarImageIsGenerated() { // start the gravvy bundle Bundle::start('gravvy'); // check that the output matches the expected $this->assertEquals(Gravvy::make('[email protected]'), '<img src="http://www.gravatar.com/avatar/fac3a58aaa455adbcb3f684ccff663b8?s=32" />'); } /** * Test that an avatars output appears as expected when * specifying a custom avatar size. * * @return void */ public function testAvatarImageIsGeneratedWithSize() { // start the gravvy bundle Bundle::start('gravvy'); // check that the output matches the expected $this->assertEquals(Gravvy::make('[email protected]', 64), '<img src="http://www.gravatar.com/avatar/fac3a58aaa455adbcb3f684ccff663b8?s=64" />'); } }
Above, we've written two PHPUnit tests: one to test the output of generating an avatar using an email, and another that also specifies an avatar size in pixels. You will notice that we call Bundle::start('gravvy')
to manually start the bundle. This is because Laravel does not auto load bundles through the command line interface at present.
As a core team member, I'd like to point out that we intend to resolve this in a future version!
Let's use Artisan to run our PHPUnit tests by typing the test
command and using the bundle name, gravvy
, as a parameter.
php artisan test gravvy
Great! Our tests have run successfully on the first try, and our ego hast grown - just a little!
Now that our Gravvy class has been tested, people can use it in their own applications! Let's take the bundle a step further and create a couple of simple pages to generate and preview gravatars. We can use this example to learn how the routing system handles bundles.
To begin, let's create a new 'preview' controller for our bundle. We will need to create a controllers
directory within the bundle, and, within it, we'll add a new file: preview.php
.
<?php class Gravvy_Preview_Controller extends Controller { /** * Show the preview avatar form. */ public function action_form() { // load the form view return View::make('gravvy::form'); } /** * Show the resulting avatar. */ public function action_preview() { // load the preview view return View::make('gravvy::preview'); } }
The controller name must be prefixed with the bundle name, and appended with _Controller
- as with normal controllers.
We could create some routes to map our controller actions to sensible URIs, but wouldn't it be better if we could let the user of our bundle decide on the base URI to use? It would? Let's do that then!
By adding a 'handles' => 'gravvy'
key-value pair to the bundles configuration array, we can allow the user to change it without altering the code of the bundle itself. Here's the resulting configuration in application/bundles.php
.
return array( 'docs' => array('handles' => 'docs'), 'gravvy' => array( 'auto' => true, 'handles' => 'gravvy' ) );
Now we can use the (:bundle)
place-holder in our routes, which will be replaced with the value of the handles
option. Let's create a routes.php
file within the root of our bundles and add some routes.
Route::get('(:bundle)/form', 'gravvy::preview@form'); Route::post('(:bundle)/preview', 'gravvy::preview@preview');
We have the route GET gravvy/form
which is mapped to the form
action of the Preview
controller, and POST gravvy/preview
which is mapped to the preview
action of the Preview
controller.
Let's create the associated views for our controller actions; you can make them as complex and pretty as you like, but I am going to keep them simple. First, create a views
folder within the bundle, just like with the application directory.
<!-- /bundles/gravvy/views/form.blade.php --> <form action="{{ URL::to_action('gravvy::preview@preview') }}" method="POST"> <p><label for="name">Email Address:</label></p> <p><input type="text" name="email" /></p> <p><label for="name">Avatar Size:</label></p> <p><input type="text" name="size" /></p> <p><input type="submit" value="Preview!" /></p> </form>
Now that we have a form that will submit an email and size field to the preview@preview
controller/action pair, let's create a preview page for the generated avatar; we'll use an attribute, named $element
, to hold its source.
<!-- /bundles/gravvy/views/preview.blade.php --> <p>{{ $element }}</p> <p>{{ HTML::link\_to\_action('gravvy::preview@form', '< Go Back!') }}</p>
Now we must alter the preview
action to make use of the data submitted from the form.
/** * Show the resulting avatar. */ public function action_preview() { // get data from our form $email = Input::get('email'); $size = Input::get('size'); // generate the avatar $avatar = Gravvy::make($email, $size); // load the preview view return View::make('gravvy::preview') ->with('element', $avatar); }
We retrieve the POST data and use it to create our avatar. We must also add a with()
method to the View::make()
chain to allow for the element to be used within the view.
We can finally test our avatar previewing system! Take a look at the /gravvy/form
URI and give it a go! Everything works as expected.
This may not be the best way to organize your bundle, but it does highlight some of the useful things that are possible. Have fun creating your own bundles, and be sure to consider publishing them on the bundles website.
Publishing a Bundle
Once your bundle is in a functional state, you may want to consider listing it within the Laravel Bundles Directory. Let's run through the process of submitting a new bundle.
First, you will need to have a GitHub account, and have your bundle versioned within a public repository. GitHub offers free accounts with an unlimited number of public repositories; you will find their sign up form here.
If you are new to version control with Git, I suggest reading the great series of Git articles right here on Nettuts+.
Once you have your account and code in order, make sure that the latest version of your bundle can be found within the 'master' branch, and that the root of your bundle (where the start.php
would be) is the root of the repository, rather than a subdirectory.
Next visit the Laravel Bundles Directory website, and sign in using your GitHub credentials.
Now click the 'Submit a Bundle' button, select your bundle repository from the drop down menu and hit the 'Continue' button.
The sign up form is quite straight forward, but here are some 'gotchas' that you may not spot.
Name
Name is a the lowercase keyword that is used to install your application. It needs to be a short but accurate word to describe your bundle.
Summary / Description
These fields can contain markdown format content. So feel free to copy the content from your GitHub README.md
file.
Dependencies / Tags
Use the comma button on your keyboard to separate tags and dependencies. The dependencies field should contain the short install keyword for the bundle that exists as a dependency for the bundle you are submitting.
Active
The Active
field simply determines whether or not the bundle will be displayed to other users. You are still able to install inactive bundles by their install keyword for testing purposes. Set this field to 'Yes' only when you are happy for other people to use your bundle.
Once you click the 'Save' button, your bundle has been submitted, and, if marked as 'Active', will appear in the bundle listings. You can always edit your bundle listing at a later date.
Finding Bundles
Bundles that have been shared with the Laravel community are listed in the Bundles directory at http://bundles.laravel.com.
You can browse bundles by category, or use the search feature to find the bundle you're looking for. Once you have found a bundle that meets your requirements, take a look at the 'Installation' tab of the bundle's profile to find the install keyword.
Installing a Bundle
Once you have the install keyword for a bundle, you can install it from the base of your project using the 'Artisan' command line interface, and it's bundle:install
command. For example..
php artisan bundle:install bob
Artisan will consult the bundles API to retrieve the path to the bundles GitHub repository, and the repositories of all its dependencies. It will then download source packages directly from GitHub, and extract them to the /bundles
directory for you.
You will need to manually add the bundle name to the array within application/bundles.php
for the bundle to become enabled.
return array( 'docs' => array('handles' => 'docs'), 'bob' );
In some situations, you might need to add extra information to this array
entry to facilitate auto starting, or directing certain routes to the bundle. The author will have provided this extra information in the bundles description, if that is the case.
Thanks for reading and enjoy creating your own bundles with Laravel! If you'd like to learn more about Laravel, be sure to pick up my book!
Comments