Last time we discussed instantiating objects in Laravel using the make
method. Today we’re going to talk about services, service containers, and binding objects into service containers.
Before we do that, we should briefly discuss service oriented architecture.
This article, and series, is focused on exploring Laravel’s internals. Many of the code samples are optimized to help you understand how Laravel is built, and may not be a best practice for day-to-day application development.
Service Oriented Architecture
Wikipedia defines Service Oriented Architecture as
Service-oriented architecture (SOA) is a software design and software architecture design pattern based on distinct pieces of software providing application functionality as services to other applications. This is known as service-orientation. It is independent of any vendor, product or technology
This is as good a definition as any. There may have been a time when “Service Oriented” refereed to a specific set of technologies, but at this point it’s a generic term that applies to a general approach to application development and/or code organization in a single application.
The best way to think about services is as an approach to programming where a service is some code that does a thing that’s not directly related to your application. A very basic example is a database querying service. If you’re writing an application to show cute cat pictures on the internet, your objects that describe a cat’s cuteness shouldn’t contain code for connecting to the database, parameterizing query strings, etc. Keeping your code for talking with a database hidden behind a service means you can concentrate on cat logic, and leave the tricky programming problems involved with talking to a database to the database service developers.
The current fashion in PHP framework development is heavily based on SOA ideas, and if you want to start thinking like a Laravel core developer you’ll want to start adopting the mindset of someone who implements services (as opposed to someone who uses services to build cute cat websites).
Service Containers
Like all abstract concepts, eventually you need to create an implementation for your service based system. The Symfony 2 project is a driving force behind popularizing the service container approach, although the idea’s been floating in the air for several years now.
A service container is an object that’s either globally available or easily “fetch-able” via anywhere you’d need to use services. A service container contains key/value pairs where the key is a service name, and the value is an object that implements a service.
If you’ve ever used Symfony 2, you’re likely familiar with service calls that look something like this
$this->get('my_mailer');
$this->getContainer()->get('my_mailer');
$this->container->get('my_mailer');
As the service user (or “client programmer”), you fetch a reference to the service container object, and then tell the service container object you want a named service (my_mailer
above).
Laravel also uses a service container, but with a few opinionated differences. First, in Laravel, the global Application
object is the server container. If Laravel had a my_mailer
service, you could access it by getting an application object reference
$app = app();
And then using PHP’s ArrayAccess syntax on the application object to fetch the service by name.
$mailer = $app['my_mailer'];
After last week’s primer you’re probably chomping at the bit to checkout Laravel’s ArrayAccess implementation. We’ll get there eventually, but for now just treat it as the magic syntactic sugar that makes code for fetching a service object more compact.
Let’s look at a real service. A stock Laravel 4.2 system comes with a db
service. If you use this service, you can run SQL queries against a database. Assuming you’ve setup Laravel to connect to a MySQL database, you can use the following code to run a SHOW TABLES
query against the database.
$app = app();
$result = $app['db']->select('SHOW TABLES');
var_dump($result);
If you use var_dump
to look at the db
service object.
var_dump($app['db']);
you’ll see the service container returns an Illuminate\Database\DatabaseManager
object
object(Illuminate\Database\DatabaseManager)[93]
protected 'app' =>
...
This object is an implementation of the db
service. One advantage of services is they’re another way to implement an “inversion of control” system. While Illuminate\Database\DatabaseManager
is the current service implementation, a future version of Laravel, or a specific Laravel application, may use a different db
service.
If you’re an experienced Laravel developer, you know there’s other ways to access services that don’t involve calling the app
function. We’re working up to those methods, we will get there eventually, but for today we’re sticking with calls to app
.
Creating a new Service
So, the question that’s likely on many of your lips is
How do services get inside the application container
This is where binding enters the story. Binding is one of those overloaded terms that means a variety of different things on different platforms. In Laravel, you bind
a service to the Application
. Binding a service to the application means other developers can access your service via the application object’s $app['service_name']
syntax.
From a high level that’s the only thing binding accomplishes. It’s nothing more complicated than that. Any first level magic user can cast this spell, no components needed. To prove it, we’re going to create a helloworld
service. Our end goal will be the ability to run code that looks like the following
$app['helloworld']->sayHello();
and have it echo out the ancient and ubiquitous text Hello, World!
.
The high level steps you need to take when creating a service are
- Pick a name (in our case,
helloworld
) - Create an implementation class
-
Bind the implementation class into the application container
Laravel has a lot of built-in classes and systems for creating packages and service providers — but in this tutorial we’re going to skip all that and concentrate on the most dead simple way to bind a service into the application container. You might use our techniques below to add a service to a single application, but you’d never use it to distribute your service to other Laravel systems.
If you’re interested in learning the right way to distribute your Laravel code, you’ll want to investigate Laravel Service Providers. Service Providers are outside of our scope for this series.
Creating the Implementation Class
We’ve already picked a name for our service (helloworld
). The next step is to create an implementation class. You can name this class anything you’d like, and place is anywhere that Laravel will autoload classes from. We’re going to name our class Pulsestorm\Example
, and to make things simple we’ll drop it in the app/models
folder. If you’re new to Laravel, app/models
is one of the default folders the autoloader will load classes from.
Create the following file in the following folder
#File: app/models/Pulsestorm/Example.php
<?php
namespace Pulsestorm;
class Example
{
protected $_times = 0;
public $message = "Hello World! We've met %d times!";
public function sayHello()
{
echo sprintf($this->message, $this->_times);
$this->_times++;
}
}
Here we have a simple class. This class implements a hello world service. There’s no special classes to inherit from or interfaces to implement — any class can be a service class. It’s good OOP practice to have service classes implement interfaces or extend from abstract classes, but Laravel doesn’t require it.
Before we bind our class as a service, let’s test it out. Add the following simple route configuration to the end of app/routes.php
.
#File: app/routes.php
Route::get('/service-tutorial', function()
{
$object = new Pulsestorm\Example;
$object->sayHello();
echo "\n<br>\n";
$object->sayHello();
});
And then load the route in your web browser (using your own application URL, of course)
http://laravel.example.com/service-tutorial
You should see output something like this
Hello World! We've met 0 times!
Hello World! We've met 1 times!
That is, the sayHello
method outputs the Hello World text, and also keeps track of how many times we’ve called the sayHello
method.
With our class working, we’re ready to move on to the service binding.
Binding the Service
As a reminder, binding a service means we’re making it available to users in the application service container. All binding a service requires us to do is call the Application
object’s bind
method. We’re going to bind our service in the app/start/global.php
file. This file runs during the application bootstrap process (although this might change in Laravel 5). By binding our service here it will be available to the rest of the application.
After all this buildup, binding a class is relatively anti-climatic. Just add the following line to the end of your app/start/global.php
file
#File: app/start/global.php
$app = app();
$app->bind('helloworld', 'Pulsestorm\Example');
That’s it. The first argument to bind
is the name you picked for your service. The second is the name of your implementation class (passed as a string).
With the above in place, let’s try calling our service from a route. Change your service-tutorial
route so it matches the following.
#File: app/routes.php
Route::get('/service-tutorial', function(){
$app = app();
$app['helloworld']->sayHello();
echo "<br>";
$app['helloworld']->sayHello();
});
And then load your route in your browser.
http://laravel.example.com/service-tutorial
If you’ve set things up correctly, you should see the following output
Hello World! We've met 0 times!
Hello World! We've met 0 times!
Success! You’ve just created your first Laravel service.
Shared Services
Let’s take another look at our output from above
Hello World! We've met 0 times!
Hello World! We've met 0 times!
You may have noticed one difference from before — the We've met n times
string didn’t increment. It echo
ed zero both times. That’s because whenever you ask the service container for this service ($object = $app['helloworld']
), Laravel will instantiate a new object for you. This makes sense as the default behavior, as it discourages developers for using services to store application state.
Sometimes though, as a service developer, you don’t want to instantiate a new object. The database service we used earlier is a classic example of this. If we instantiated a new database service every time we asked the application for a service, that would mean a new database connection would be instantiated every time we ran a query. A page with ten queries would try to connect to the database 10 times, and with a database connection being such an expensive operation, you’d quickly hit scaling or performance problems.
Fortunately, Laravel has the concept of “shared” services. If a service is a shared service, it means Laravel will always return the same instance whenever you ask for the service. In some object systems this concept is called a singleton.
If you didn’t follow that, an example should clear it up. To tell Laravel you want your service to be a shared service, you simply pass in a third parameter at bind time. Change your application binding so it looks like the following.
#File: app/start/global.php
$app = app();
$app->bind('helloworld', 'Pulsestorm\Example', true);
See that third parameter? The boolean true
? If you look at bind
‘s method prototype
#File: vendor/laravel/framework/src/Illuminate/Container/Container.php
public function bind($abstract, $concrete = null, $shared = false)
{
//...
}
You can see the third parameter is named $shared
. This parameter tells bind
we want a shared service.
With the above parameter added to the bind
method call, try reloading your page. You should see the following
Hello World! We've met 0 times!
Hello World! We've met 1 times!
As you can see, the service is keeping track of state between requests, and correctly incrementing the “times met” property.
Important: Having our hello world service track state like this would be a bad implementation of service oriented architecture — in general the only state a service should track is state it needs to do its job, not state to implement application functionality.
There’s no hard and fast rules as to when you should use a shared service. The services Laravel itself ships with are a combination of shared and unshared. You’ll need to use your best judgment when creating your own service. If you need an algorithmic methodology, stick to not using shared services until you start noticing performance problems with your service. Then, consider a shared service to solve your performance problems.
Binding Closures
If we take another look at our bind
call
#File: app/start/global.php
$app = app();
$app->bind('helloworld', 'Pulsestorm\Example', true);
Another, more subtle, problem pops out. Because we’re passing our class name in as a string, there’s no chance for us to do any initialization of our service object. Fortunately, Laravel has a solution for this. In addition to accepting a string, the bind
method also accepts a PHP anonymous function as an argument. Try replacing your binding code with the following.
#File: app/start/global.php
$callback = function(){
$implementation = new Pulsestorm\Example;
$implementation->message = 'Hello world, we meet again (time number %d)';
return $implementation;
};
$app = app();
$app->bind('helloworld', $callback);
If you reload your page you should see the following text.
Hello world, we meet again (time number 0)
Hello world, we meet again (time number 0)
That is, we’ve successfully bound a service with an anonymous function. Here’s how the above code works. First, and most importantly, we define an anonymous function
#File: app/start/global.php
$callback = function(){
$implementation = new Pulsestorm\Example;
$implementation->message = 'Hello world, we meet again (time number %d)';
return $implementation;
};
This function should return an instance of our service implementation. Since we’re instantiating the object ourselves, this gives us a chance to perform a number of initializations steps. (i.e. setting the messages
property).
Once we’ve created this anonymous function, we pass it into bind
#File: app/start/global.php
$app = app();
$app->bind('helloworld', $callback);
Once we’ve done this, the next time we request the helloworld
service, Laravel will invoke/call the anonymous function — whatever the anonymous function returns is what our request for a service will return.
Shared Service Bindings with Closures
In our example above we omitted the third parameter from bind
. This meant our service was an un-shared service, and returned new objects each time we requested a service from the application object. Services created with anonymous functions are eligible to be marked as shared, same as any other service. If we wanted a shared service, providing the third method parameter would work
#File: app/start/global.php
$app->bind('helloworld', $callback, true);
However, if you’re binding anonymous functions as services, you have another option for creating a shared service, and that’s the bindShared
method. Replace your binding code with the following
#File: app/start/global.php
$callback = function(){
$implementation = new Pulsestorm\Example;
$implementation->message = 'Hello world, we meet again (time number %d)';
return $implementation;
};
$app = app();
$app->bindShared('helloworld', $callback);
All we’ve done here is changed the call to the $app->bind
method into a call to the $app->bindShared
method. Reload with the above in place, and we’ve got our shared/singleton behavior back
Hello world, we meet again (time number 0)
Hello world, we meet again (time number 1)
The bindShared
method is the preferred way of assigning a shared service to the container, and the Laravel core code uses it extensively. As a quick example, take a look at this service provider code where Laravel binds the database service
#File: vendor/laravel/framework/src/Illuminate/Database/DatabaseServiceProvider.php
//...
$this->app->bindShared('db', function($app)
{
return new DatabaseManager($app, $app['db.factory']);
});
//...
Here the Laravel core users bindShare
to bind a DatabaseManager
object as the db
service.
Non-Object Services
There’s one curious side effect to Laravel’s anonymous function service providers. Consider a service bound like this
#File: app/global/start.php
$app->bindShared('curious', function(){
return 'Curious Service is Curious';
});
and used like this
#File: app/routes.php
Route::get('/service-tutorial', function(){
$app = app();
$curious = $app['curious'];
var_dump($curious);
});
The above code will output the text
string 'Curious Service is Curious' (length=26)
That is, despite us requesting a service object, the service container returns a string. It’d be easy to write this “string service” off as an unintended side effect of Laravel’s anonymous function implementation, but the core application code itself relies on this behavior. Give the following a try
#File: app/routes.php
Route::get('/service-tutorial', function(){
$app = app();
var_dump($app['env']);
});
Loading the tutorial
route in a browser with the above code results in output something like the following
string 'local' (length=5)
Your system may output the text production
, or development
, or something else. This env
service is how Laravel keeps track of the current environment, and knows which configuration files to load. It’s also why the resolving listener in our last article received a string instead of an object.
The takeaway here is that despite being billed as a service object container, Laravel’s service container will sometimes return a scaler value (boolean, string, int, etc). In other words, you can’t assume it will always return an object. You’ll want to be defensive in your resolvingCallbacks
, or anywhere you’re not familiar with the behavior of a particular service.
Laravel Services, make
, and ArrayAccess
There’s one last thing we’ll want to cover about services before we finish up for today. In the first article of this series, we covered using the application object’s make
factory to create objects. What we didn’t mention in that tutorial was that make
, in addition to accepting class names, also accepts service names.
Assuming you’ve bound a service to helloworld
, give the following a try.
#File: app/routes.php
Route::get('/service-tutorial', function(){
$app = app();
$app->make('helloworld')->sayHello();
echo "<br>\n";
$app->make('helloworld')->sayHello();
});
This will produce the exact same results as asking the application container for the service
Hello world, we meet again (time number 0)
Hello world, we meet again (time number 1)
The call to make
also obeys any shared binding. Put another way, you can use make to create a Laravel object from a PHP class, or from a service container alias.
This is more than side effect behavior. So far we’ve accessed services via the app container’s ArrayAccess syntax.
$app['helloworld'];
When you try to get
a value from an object with array syntax, PHP calls the object’s offsetGet
method. If we take a look at the Container
class’s offsetGet
method (the Application
class extends from the Container
class), we see the following.
#File: vendor/laravel/framework/src/Illuminate/Container/Container.php
public function offsetGet($key)
{
return $this->make($key);
}
If it’s not immediately obvious to you — what the code above means is whenever we’ve called $app['helloworld']
, behind the scenes Laravel’s just called $app->make('helloworld');
While interesting to systems developers, this creates a lot of confusion as to what “the right” way to access a Laravel service is, or how a service package developer should tell their users to access the service. We’ll cover this, and more, in our next article, when we talk about (cue nerd fight) Laravel Facades.