Services in CodeIgniter 4

In the last article we looked at Dependency Injection and how it is used within CodeIgniter, as well as guidelines on how to make use of it in your own applications. As promised, this time we’re diving deeper into the subject and talking about Services in CodeIgniter 4.

So – what are Services, anyway?

Services are simply dependencies in your project. Using a Service, instead of just creating a new instance in the controller, gives you three primary benefits:

  1. Allows you to always get the same instance no matter how many times you call that Service. If you have a UserModel, you don’t need 5 different instances, that will just be a waste of resources, and might make things get confused. Of course, you have the option to get a new instance whenever you call a Service, also. By default, Services always return shared instances.

  2. You have a single place to setup the Service’s dependencies. If you have a UserManager class that needs an instance of a UserModel as one of its dependencies, you don’t want to have specify that every time. Instead, you take care of that, and any other setup (like passing in a Logger instance), within the Service method, and – voila! – it’s all managed in a single place.

  3. If you ever need to swap out the service for another class that shares the same Interface, then you change that in the Service file, and it is now changed everywhere that you might have used that in your application. Neat.

Looking at the files

All of the core Services that make up the framework in stored in system/Config/Services.php. Each section of the framework (like logging, the view parser, etc) have a Service that you should use to access it.

In your application, there is an empty skeleton file at application/Config/Services.php. This is where you would put your application’s Services. We will discuss when you might consider creating a Service below. Also – if you need to override any part of the framework, you can copy the method from the system Services file into the application Services file, make your changes, and then your custom service will be used anytime the system tries to use that part of the application.

Calling a Service

Services are a simple static method in a class that returns a class instance. Whenever you need to grab a Service instance, you would do:

$cell = ConfigServices::viewcell();

That returns a shared instance that will be the same no matter where it’s called. If, for some reason, you want another copy of the class, pass false as the last argument:

$cell = ConfigServices::viewcell(false);

Many times, a Service can take additional arguments to customize the class, pass different config files, dependencies, etc in. The View service can take the path to the folder of view files as it’s first argument:

$view = ConfigServices::view(APPPATH.'views/', false);

Anytime you call a service, you should use the ConfigServices class, no matter which Services file the service is actually stored in. CodeIgniter will do its magic in the background to make sure that the correct service is returned to you.

Creating a Service

Creating a new Service is simple. Let’s say you want to make a UserManager class a Service. Open up application/Config/Services.php and add a new static method:

public static function userManager($getShared = true) 
{
   if ($getShared)
   {
      return self::getSharedInstance('userManager');
   }

   return new App\Users\UserManager();
}

Every Service should take a $getShared value as true, to determine whether a single, shared instance is returned or a new one. The first thing you do in the method itself is to check for a shared instance. the getSharedInstance method takes the name of the Service/method, and any arguments (except for $getShared). If an instance has already been created, it returns that instance, otherwise it calls this method again with $getShared = false, gets the new instance, caches it in the class, and then returns that instance.

Let’s say you want to add logging to the class as a dependency. You might do it like this:

public static function userManager(Logger $logger = null, $getShared = true) 
{
   if ($getShared)
   {
      return self::getSharedInstance('userManager', $logger);
   }

   if (is_null($logger))
   {
      $logger = self::logger();
   }

   $manager = new App\Users\UserManager();
   $manager->setLogger($logger);
   
   return $manager;
}

This accepts a logger instance in the Service method itself, and creates default version if none is specified. It then creates the user class and passes the logger instance in through the setLogger() method. Notice, also, that the $logger value was passed to the getSharedInstance() method.

When to Create a Service

It might be tempting to create services for every class in your application, and you could do that. It would make it very flexible. But that’s often overkill. Here are some things to consider when deciding whether you need a service or not:

  1. Are there multiple implementations of this class? If so – then yes, create a service. That way it’s simple to swap them out down the road if needed.

  2. Are you going to want to mock this class during testing? If so – then create a service. During testing you can use the Services::injectMock() method to specify the class instance that should be used when running the test.

  3. Will you use this in multiple places, but only want/need a single instance? Then create a service. However, this one’s a little less cut and dried than the others. If all of the places it will be used are created/called from the same controller method, than you might not need a service since you can grab an instance of the class there and pass it in as a dependency to the other classes.

Overriding an Existing Service

Let’s take a quick look at overriding the Logging system to use Monolog, per one of the Insider’s request. Note, I haven’t used the package much in the past, and I’m not actually implementing this so don’t know this will work exactly like I’m showing, but it presents the concepts. Final debugging is left to anyone who wants to implement this