Part 2. Refactoring the Service Loader.
One of the most important things you can do as a developer is refactoring. When you get to a point where a piece of code is working stop and look for ways to make it better. This may encompass things like removing duplicated code, breaking up large methods or classes, or even restructuring to make things easier to locate. These types of changes will make your life easier later on when you need to add functionality or fix a bug in your code.
Let’s do some refactoring.
Encapsulation.
We are currently using PHP to do a directory search of the file system for things to load into our Container. Let’s encapsulate this functionality and abstract it. Doing so will make it more understandable and push a lot of the low-level stuff like checking if directories exist, filtering out ‘..’ and ‘.’, and building paths into helper methods.
There is a great library from Symfony that can help us called Finder. Finder searches the file system for files and directories using a fluent interface and returns the results as a PHP Iterator. Installing it is easy using Composer.
$ composer require symfony/finder
Now we can go back to our Loader files and refactor some code so that we can utilize this library.
Duplication and Refactoring.
All three of our Loader classes basically do the same thing. They all get files from a directory and load definitions into the Container. Right now we call each type and location explicitly. This is code duplication that we can get rid of. We will need to change how we deliver these paths so that we can load the definitions more dynamically. One method to load them all.
Let’s start by rewriting the ServiceLoader
class. We will be reusing this class to do all of our loading for us. The first thing we need to do is add a use statement for the Symfony Finder class so we can utilize it. Add the following just under the existing use statements.
use Symfony\Component\Finder\Finder;
Now add a private property called $finder
to the class like so. This will hold our instance of the Finder.
/** @var \Symfony\Component\Finder\Finder $finder */ private $finder;
And pass an instance of Finder into the constructor and set it to our new private property.
public function __construct(Finder $finder){ $this->finder = $finder; }
Next, let’s refactor our setting.php
and put the service paths into an array. This will make it possible for us to pass them to the Loader and let it iterate through them dynamically.
Change
'service_config_dir' => APP_ROOT.'/configuration/services/', 'middleware_config_dir' => APP_ROOT.'/configuration/middleware/', 'route_config_dir' => APP_ROOT.'/configuration/routes/',
To
'service_directories' => [ 'services' => APP_ROOT.'/configuration/services/', 'middlewares' => APP_ROOT.'/configuration/middleware/', 'routes' => APP_ROOT.'/configuration/routes/', ],
We will need to pass the directory names into the ServiceLoader
method now that we are going to be reusing it for all loading. Let’s change the loadServices
method to take an instance of the Application as its first parameter, and an array of directories as its second.
Add use App\Application as Application;
to the use block at the top of the ServiceLoader.php
file. Then change the signature of the method like so.
public function loadServices(Application $app, $directories)
Inside the method, let’s utilize the Finder object to load the service definitions like so.
$this->finder->files()->name('*.php')->depth(0)->in($directories); foreach ($this->finder as $file) { require $file->getRealpath(); } return $app;
In this case, we take the finder object and tell it we want to find files. These files have names ending in .php in the root (0 depth) of the directories given. This is where the change to our settings file comes into play. We will iterate over these directories, grabbing each file, and pull them in as a required file utilizing their real pathname.
Now we see the power of the Symfony Finder. It allows us to search directory paths for defined patterns and retrieve the real paths of those files. All with a couple of lines of code.
Now, let’s finish it off in our Application.php
. First, add a use statement to the file for the Finder class just like you did for the ServiceLoader
class above. Then remove the loadMiddleware
and loadRoutes
methods and their calls. Finally, change the loadServices
class to pass in an instance of the application and the directory array. The loadServices
method should look something like this.
private function loadServices() { $serviceLoader = new ServiceLoader(new Finder); $serviceLoader->loadServices( $this, $this->getContainer()->get("settings")["service_directories"] ); }
Now, one final change. In the services.php
file lets add a doc block to the $container
variable. While not adding any functionality it adds clarity to what object the variable holds. It will also help some IDEs map the variable to the class that defines it, giving better code completion.
/** @var \Interop\Container\ContainerInterface $container */ $container = $app->getContainer();
Finished.
We are now loading all of our definitions into the container dynamically with a single method. Very clean and simple. This gives us a lot more flexibility in how we define and organize our service definitions. We can group similar services into corresponding files or separate each of them into their own file. There is no Right way to organize them, and how they are organized should be determined by the project’s needs.
In the next installment, we will see how to decouple configurations and settings that are environment-specific and store them outside of version control.
Code.
See the final code for this article on github. 002 Refactoring the file loader